diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 7088a97dc9..3a28a6e403 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -440,6 +440,39 @@ jobs:
- name: MySQL Tests
run: dotnet test ./tests/Paramore.Brighter.MySQL.Tests/Paramore.Brighter.MySQL.Tests.csproj --filter "Fragile!=CI" --configuration Release --logger "console;verbosity=normal" --logger GitHubActions --blame -v n
+ oracle-ci:
+ runs-on: ubuntu-latest
+ timeout-minutes: 12
+ needs: [build]
+
+ services:
+ oracle:
+ image: gvenzl/oracle-free:latest
+ ports:
+ - 1521:1521
+ env:
+ ORACLE_PASSWORD: oracle
+ APP_USER: brighter
+ APP_USER_PASSWORD: BrighterTests1!
+ options: >-
+ --health-cmd healthcheck.sh
+ --health-interval 10s
+ --health-timeout 5s
+ --health-retries 30
+
+ steps:
+ - uses: actions/checkout@v7
+ - name: Setup dotnet
+ uses: actions/setup-dotnet@v5
+ with:
+ dotnet-version: |
+ 9.0.x
+ 10.0.x
+ - name: Grant DBMS_LOCK to brighter user
+ run: docker exec -i "${{ job.services.oracle.id }}" sqlplus -s "/ as sysdba" @/dev/stdin < docker/oracle-init/01-grants.sql
+ - name: Oracle Tests
+ run: dotnet test ./tests/Paramore.Brighter.Oracle.Tests/Paramore.Brighter.Oracle.Tests.csproj --filter "Fragile!=CI" --configuration Release --logger "console;verbosity=normal" --logger GitHubActions --blame -v n
+
dynamo-ci:
runs-on: ubuntu-latest
timeout-minutes: 8
diff --git a/Brighter.slnx b/Brighter.slnx
index 794ccea98c..b36ace71ba 100644
--- a/Brighter.slnx
+++ b/Brighter.slnx
@@ -223,6 +223,7 @@
+
@@ -250,6 +251,7 @@
+
@@ -266,6 +268,7 @@
+
@@ -276,6 +279,7 @@
+
@@ -303,6 +307,7 @@
+
@@ -310,6 +315,7 @@
+
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 34b9e51b87..516a1b8044 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -127,6 +127,8 @@
+
+
diff --git a/docker-compose-oracle.yaml b/docker-compose-oracle.yaml
new file mode 100644
index 0000000000..4d4a3f3439
--- /dev/null
+++ b/docker-compose-oracle.yaml
@@ -0,0 +1,17 @@
+# Oracle Database Free (gvenzl/oracle-free)
+#
+# First startup may take a few minutes while the database is initialized.
+# Connection string (ODP.NET):
+# Data Source=//localhost:1521/FREEPDB1;User Id=system;Password=BrighterTests1!;
+
+services:
+ oracle:
+ image: gvenzl/oracle-free:slim
+ ports:
+ - "1521:1521"
+ volumes:
+ - ./docker/oracle-init:/container-entrypoint-initdb.d
+ environment:
+ ORACLE_RANDOM_PASSWORD: true
+ APP_USER: brighter
+ APP_USER_PASSWORD: BrighterTests1!
diff --git a/docker/oracle-init/01-grants.sql b/docker/oracle-init/01-grants.sql
new file mode 100644
index 0000000000..6d0891c709
--- /dev/null
+++ b/docker/oracle-init/01-grants.sql
@@ -0,0 +1,2 @@
+ALTER SESSION SET CONTAINER = FREEPDB1;
+GRANT EXECUTE ON DBMS_LOCK TO brighter;
diff --git a/src/Paramore.Brighter.BoxProvisioning.Oracle/IOracleAdvisoryLock.cs b/src/Paramore.Brighter.BoxProvisioning.Oracle/IOracleAdvisoryLock.cs
new file mode 100644
index 0000000000..279158b4d9
--- /dev/null
+++ b/src/Paramore.Brighter.BoxProvisioning.Oracle/IOracleAdvisoryLock.cs
@@ -0,0 +1,75 @@
+// The MIT License (MIT)
+// Copyright © 2026 Ian Cooper
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using Oracle.ManagedDataAccess.Client;
+
+namespace Paramore.Brighter.BoxProvisioning.Oracle;
+
+///
+/// Encapsulates the Oracle session-level advisory-lock primitive used to serialise
+/// box-table migrations. Backed by DBMS_LOCK.REQUEST / DBMS_LOCK.RELEASE.
+/// Requires the executing user to hold EXECUTE ON DBMS_LOCK (or
+/// EXECUTE ANY PROCEDURE).
+///
+///
+/// The default implementation is .
+/// accepts an injected instance through its constructor for testability and for advanced
+/// integrators. Lock-key derivation is performed by the runner via
+/// ; the abstraction owns only the
+/// DBMS_LOCK.REQUEST / DBMS_LOCK.RELEASE PL/SQL calls.
+///
+public interface IOracleAdvisoryLock
+{
+ ///
+ /// Acquires a session-level exclusive advisory lock via DBMS_LOCK.REQUEST.
+ ///
+ /// The open on which to run the PL/SQL block.
+ /// The lock name; must be within the 128-character DBMS_LOCK.ALLOCATE_UNIQUE limit.
+ /// Callers should derive via .
+ /// Maximum wait. Values are truncated to whole seconds; zero means
+ /// try-once (NOWAIT).
+ /// Cancellation token observed by the underlying command.
+ /// Thrown when DBMS_LOCK.REQUEST returns 1
+ /// (could not acquire within ).
+ /// Thrown when DBMS_LOCK.REQUEST returns
+ /// 2 (deadlock), 3 (parameter error), or 5 (illegal handle).
+ Task AcquireAsync(
+ OracleConnection connection, string lockKey,
+ TimeSpan timeout, CancellationToken cancellationToken);
+
+ ///
+ /// Releases the advisory lock via DBMS_LOCK.RELEASE.
+ ///
+ /// The same on which the lock was acquired.
+ /// The lock name used at acquisition.
+ /// Cancellation token.
+ ///
+ /// true — released successfully (code 0).
+ /// false — not the owner or parameter error (codes 3–5).
+ /// null — lock was never acquired (no handle allocated for this session).
+ ///
+ Task ReleaseAsync(
+ OracleConnection connection, string lockKey,
+ CancellationToken cancellationToken);
+}
diff --git a/src/Paramore.Brighter.BoxProvisioning.Oracle/OracleAdvisoryLock.cs b/src/Paramore.Brighter.BoxProvisioning.Oracle/OracleAdvisoryLock.cs
new file mode 100644
index 0000000000..510aab29fb
--- /dev/null
+++ b/src/Paramore.Brighter.BoxProvisioning.Oracle/OracleAdvisoryLock.cs
@@ -0,0 +1,194 @@
+// The MIT License (MIT)
+// Copyright © 2026 Ian Cooper
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+using System.Data;
+using System.Threading;
+using System.Threading.Tasks;
+using Oracle.ManagedDataAccess.Client;
+
+namespace Paramore.Brighter.BoxProvisioning.Oracle;
+
+///
+/// Default backed by Oracle DBMS_LOCK.ALLOCATE_UNIQUE,
+/// DBMS_LOCK.REQUEST, and DBMS_LOCK.RELEASE.
+///
+///
+///
+/// The connecting user must hold EXECUTE ON DBMS_LOCK (or
+/// EXECUTE ANY PROCEDURE). Both ALLOCATE_UNIQUE and REQUEST are called
+/// in the same session so the allocated handle is stable for the lock's lifetime.
+///
+///
+/// Because ALLOCATE_UNIQUE is idempotent for the same lock name within the same
+/// database, ReleaseAsync re-allocates the handle for the given
+/// before releasing — this keeps the interface stateless and avoids storing per-session state
+/// on the instance.
+///
+///
+/// DBMS_LOCK.REQUEST return codes:
+/// 0 = success, 1 = timeout, 2 = deadlock, 3 = parameter error, 4 = already held (same
+/// session), 5 = illegal lock handle.
+///
+///
+public class OracleAdvisoryLock : IOracleAdvisoryLock
+{
+ ///
+ public async Task AcquireAsync(
+ OracleConnection connection, string lockKey,
+ TimeSpan timeout, CancellationToken cancellationToken)
+ {
+ if (timeout < TimeSpan.Zero)
+ {
+ throw new ArgumentOutOfRangeException(nameof(timeout), timeout,
+ "Migration lock timeout must be non-negative.");
+ }
+
+ var timeoutSeconds = (int)Math.Ceiling(timeout.TotalSeconds);
+ var lockHandle = await AllocateLockHandleAsync(connection, lockKey, cancellationToken);
+ if (string.IsNullOrEmpty(lockHandle))
+ {
+ throw new OracleAdvisoryLockException($"DBMS_LOCK.ALLOCATE_UNIQUE returned an empty handle for '{lockKey}'.");
+ }
+
+ var result = await RequestLockAsync(connection, lockHandle!, timeoutSeconds, cancellationToken);
+
+ switch (result)
+ {
+ case 0:
+ case 4:
+ return;
+ case 1: throw new TimeoutException(
+ $"Could not acquire migration lock '{lockKey}' within {timeoutSeconds}s.");
+ case 2: throw new OracleAdvisoryLockException(
+ $"DBMS_LOCK.REQUEST on '{lockKey}' detected a deadlock (return code 2).");
+ default: throw new OracleAdvisoryLockException(
+ $"DBMS_LOCK.REQUEST on '{lockKey}' failed with return code {result}.");
+ }
+ }
+
+ ///
+ public async Task ReleaseAsync(
+ OracleConnection connection, string lockKey,
+ CancellationToken cancellationToken)
+ {
+ try
+ {
+ var lockHandle = await AllocateLockHandleAsync(connection, lockKey, cancellationToken);
+ if (string.IsNullOrEmpty(lockHandle))
+ {
+ return null;
+ }
+
+ var result = await ReleaseLockByHandleAsync(connection, lockHandle!, cancellationToken);
+ return result == 0;
+ }
+ catch
+ {
+ return null;
+ }
+ }
+
+ private static async Task AllocateLockHandleAsync(
+ OracleConnection connection, string lockKey,
+ CancellationToken cancellationToken)
+ {
+ const string plsql = """
+ BEGIN
+ DBMS_LOCK.ALLOCATE_UNIQUE(:name, :handle);
+ END;
+ """;
+
+#if NETFRAMEWORK
+ using var cmd = new OracleCommand(plsql, connection);
+#else
+ await using var cmd = new OracleCommand(plsql, connection);
+#endif
+
+ var handleParam = new OracleParameter("handle", OracleDbType.Varchar2, 128)
+ {
+ Direction = ParameterDirection.Output
+ };
+
+ cmd.Parameters.Add(
+ new OracleParameter("name", OracleDbType.Varchar2)
+ {
+ Value = lockKey,
+ Direction = ParameterDirection.Input
+ });
+ cmd.Parameters.Add(handleParam);
+
+ await cmd.ExecuteNonQueryAsync(cancellationToken);
+ return handleParam.Value?.ToString();
+ }
+
+ private static async Task RequestLockAsync(
+ OracleConnection connection,
+ string lockHandle,
+ int timeoutSeconds,
+ CancellationToken cancellationToken)
+ {
+ const string plsql = """
+ BEGIN
+ :result := DBMS_LOCK.REQUEST(:lockhandle, 6, :timeout, FALSE);
+ END;
+ """;
+
+#if NETFRAMEWORK
+ using var cmd = new OracleCommand(plsql, connection);
+#else
+ await using var cmd = new OracleCommand(plsql, connection);
+#endif
+
+ var resultParam = new OracleParameter("result", OracleDbType.Int32, ParameterDirection.Output);
+ cmd.Parameters.Add(resultParam);
+ cmd.Parameters.Add("lockhandle", OracleDbType.Varchar2, lockHandle, ParameterDirection.Input);
+ cmd.Parameters.Add("timeout", OracleDbType.Int32, timeoutSeconds, ParameterDirection.Input);
+
+ await cmd.ExecuteNonQueryAsync(cancellationToken);
+ return Convert.ToInt32(resultParam.Value?.ToString());
+ }
+
+ private static async Task ReleaseLockByHandleAsync(
+ OracleConnection connection,
+ string lockHandle,
+ CancellationToken cancellationToken)
+ {
+ const string plsql = """
+ BEGIN
+ :result := DBMS_LOCK.RELEASE(:lockhandle);
+ END;
+ """;
+
+#if NETFRAMEWORK
+ using var cmd = new OracleCommand(plsql, connection);
+#else
+ await using var cmd = new OracleCommand(plsql, connection);
+#endif
+
+ var resultParam = new OracleParameter("result", OracleDbType.Int32, ParameterDirection.Output);
+ cmd.Parameters.Add(resultParam);
+ cmd.Parameters.Add("lockhandle", OracleDbType.Varchar2, lockHandle, ParameterDirection.Input);
+
+ await cmd.ExecuteNonQueryAsync(cancellationToken);
+ return Convert.ToInt32(resultParam.Value?.ToString());
+ }
+}
diff --git a/src/Paramore.Brighter.BoxProvisioning.Oracle/OracleAdvisoryLockException.cs b/src/Paramore.Brighter.BoxProvisioning.Oracle/OracleAdvisoryLockException.cs
new file mode 100644
index 0000000000..e1fcccc44d
--- /dev/null
+++ b/src/Paramore.Brighter.BoxProvisioning.Oracle/OracleAdvisoryLockException.cs
@@ -0,0 +1,43 @@
+// The MIT License (MIT)
+// Copyright © 2026 Ian Cooper
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+
+namespace Paramore.Brighter.BoxProvisioning.Oracle;
+
+///
+/// Thrown when DBMS_LOCK.REQUEST returns an error code that is not a timeout.
+/// Distinct from so operators can tell a contended lock
+/// (timeout, code 1) apart from a server-side error (deadlock code 2, parameter error
+/// code 3, illegal handle code 5).
+///
+public class OracleAdvisoryLockException : Exception
+{
+ ///
+ /// Initializes a new instance of .
+ ///
+ public OracleAdvisoryLockException(string message) : base(message) { }
+
+ ///
+ /// Initializes a new instance of with an inner exception.
+ ///
+ public OracleAdvisoryLockException(string message, Exception inner) : base(message, inner) { }
+}
diff --git a/src/Paramore.Brighter.BoxProvisioning.Oracle/OracleBoxDetectionHelper.cs b/src/Paramore.Brighter.BoxProvisioning.Oracle/OracleBoxDetectionHelper.cs
new file mode 100644
index 0000000000..058bc81b5c
--- /dev/null
+++ b/src/Paramore.Brighter.BoxProvisioning.Oracle/OracleBoxDetectionHelper.cs
@@ -0,0 +1,235 @@
+// The MIT License (MIT)
+// Copyright © 2026 Ian Cooper
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using Oracle.ManagedDataAccess.Client;
+
+namespace Paramore.Brighter.BoxProvisioning.Oracle;
+
+///
+/// Oracle detection queries used by the box provisioners (pre-lock) and the migration
+/// runner (under DBMS_LOCK). Oracle identifiers are stored in uppercase by the data
+/// dictionary; comparisons against ALL_TABLES and ALL_TAB_COLUMNS are performed
+/// with UPPER() so callers may pass mixed-case names. The column set comparer is
+/// to match PascalCase LogicalColumns
+/// stored on each migration against Oracle's uppercase column name values.
+///
+///
+/// Oracle DDL has implicit per-statement commit, so the OracleTransaction parameter
+/// is accepted but not consumed. The runner relies on DBMS_LOCK for mutual exclusion
+/// across instances.
+///
+/// When is null, queries resolve the owner via
+/// SYS_CONTEXT('USERENV','CURRENT_SCHEMA'). Stateless service; safe to register as
+/// a DI singleton.
+///
+///
+public class OracleBoxDetectionHelper :
+ IAmAVersionDetectingMigrationHelper
+{
+ ///
+ /// Returns true if a table with the given name exists in the given schema.
+ ///
+ /// Optional. Null is resolved to SYS_CONTEXT('USERENV','CURRENT_SCHEMA').
+ /// Accepted and ignored — Oracle DDL auto-commits.
+ public async Task DoesTableExistAsync(
+ OracleConnection connection, string tableName, string? schemaName,
+ CancellationToken cancellationToken = default,
+ OracleTransaction? transaction = null)
+ {
+ var resolvedSchema = await ResolveSchemaAsync(connection, schemaName, cancellationToken);
+
+ using var command = (OracleCommand)connection.CreateCommand();
+ command.BindByName = true;
+ command.CommandText = @"
+SELECT COUNT(1) FROM ALL_TABLES
+WHERE OWNER = UPPER(:SchemaName) AND TABLE_NAME = UPPER(:TableName)";
+ command.Parameters.Add(new OracleParameter("SchemaName", resolvedSchema));
+ command.Parameters.Add(new OracleParameter("TableName", tableName));
+
+ var result = await command.ExecuteScalarAsync(cancellationToken);
+ return Convert.ToInt32(result) > 0;
+ }
+
+ ///
+ /// Returns true if the migration history table exists and has at least one row for the given
+ /// box table.
+ ///
+ /// Optional. Null is resolved via SYS_CONTEXT.
+ /// Accepted and ignored — Oracle keeps history in the
+ /// connected user's schema; is a no-op.
+ /// Accepted and ignored — Oracle DDL auto-commits.
+ public async Task DoesHistoryExistAsync(
+ OracleConnection connection, string tableName, string? schemaName, string? historySchema,
+ CancellationToken cancellationToken = default,
+ OracleTransaction? transaction = null)
+ {
+ _ = historySchema;
+ var historyTableExists = await DoesTableExistAsync(
+ connection, "__BRIGHTERMIGRATIONHISTORY", schemaName, cancellationToken);
+ if (!historyTableExists)
+ return false;
+
+ var resolvedSchema = await ResolveSchemaAsync(connection, schemaName, cancellationToken);
+
+ using var command = (OracleCommand)connection.CreateCommand();
+ command.BindByName = true;
+ command.CommandText = @"
+SELECT COUNT(1) FROM __BRIGHTERMIGRATIONHISTORY
+WHERE BoxTableName = :BoxTableName AND SchemaName = :SchemaName";
+ command.Parameters.Add(new OracleParameter("BoxTableName", tableName));
+ command.Parameters.Add(new OracleParameter("SchemaName", resolvedSchema));
+
+ var raw = await command.ExecuteScalarAsync(cancellationToken);
+ return Convert.ToInt32(raw) > 0;
+ }
+
+ ///
+ /// Returns the highest migration version recorded in history for the given box table, or 0
+ /// if no rows exist.
+ ///
+ /// Optional. Null is resolved via SYS_CONTEXT.
+ /// Accepted and ignored — Oracle keeps history in the connected user's schema.
+ /// Accepted and ignored — Oracle DDL auto-commits.
+ public async Task GetMaxVersionAsync(
+ OracleConnection connection, string tableName, string? schemaName, string? historySchema,
+ CancellationToken cancellationToken = default,
+ OracleTransaction? transaction = null)
+ {
+ _ = historySchema;
+ var resolvedSchema = await ResolveSchemaAsync(connection, schemaName, cancellationToken);
+
+ using var command = (OracleCommand)connection.CreateCommand();
+ command.BindByName = true;
+ command.CommandText = @"
+SELECT COALESCE(MAX(MigrationVersion), 0) FROM __BRIGHTERMIGRATIONHISTORY
+WHERE BoxTableName = :BoxTableName AND SchemaName = :SchemaName";
+ command.Parameters.Add(new OracleParameter("BoxTableName", tableName));
+ command.Parameters.Add(new OracleParameter("SchemaName", resolvedSchema));
+
+ return Convert.ToInt32(await command.ExecuteScalarAsync(cancellationToken));
+ }
+
+ ///
+ /// Reads the column name set for the given table from ALL_TAB_COLUMNS.
+ /// Oracle stores column names in uppercase; the returned set uses
+ /// for PascalCase comparisons.
+ ///
+ /// Optional. Null is resolved via SYS_CONTEXT.
+ /// Accepted and ignored — Oracle DDL auto-commits.
+ public async Task> GetTableColumnsAsync(
+ OracleConnection connection, string tableName, string? schemaName,
+ CancellationToken cancellationToken = default,
+ OracleTransaction? transaction = null)
+ {
+ return await GetTableColumnsAsHashSetAsync(connection, tableName, schemaName, cancellationToken);
+ }
+
+ ///
+ /// Detects the current logical schema version of a box table by inspecting its column set.
+ /// Returns one of three values per ADR 0057 §3:
+ ///
+ /// - -1 when the discriminator column is absent (the table is not a Brighter box).
+ /// - 0 when the discriminator is present but the V1 column set is incomplete (unknown schema).
+ /// - V >= 1 for the highest version whose cumulative LogicalColumns is a subset of the actual columns.
+ ///
+ ///
+ /// Selects the discriminator: HeaderBag for outbox, CommandBody for inbox.
+ /// Optional. Null is resolved via SYS_CONTEXT.
+ /// Accepted and ignored — Oracle DDL auto-commits.
+ public async Task DetectCurrentVersionAsync(
+ OracleConnection connection, string tableName, string? schemaName,
+ BoxType boxType, IReadOnlyList migrations,
+ CancellationToken cancellationToken = default,
+ OracleTransaction? transaction = null)
+ {
+ var actualColumns = await GetTableColumnsAsHashSetAsync(
+ connection, tableName, schemaName, cancellationToken);
+
+ var discriminator = DiscriminatorFor(boxType);
+ if (!actualColumns.Contains(discriminator))
+ return -1;
+
+ if (migrations.Count == 0 || !actualColumns.IsSupersetOf(migrations[0].LogicalColumns))
+ return 0;
+
+ var matched = migrations[0].Version;
+ for (var i = 1; i < migrations.Count; i++)
+ {
+ if (!actualColumns.IsSupersetOf(migrations[i].LogicalColumns))
+ break;
+ matched = migrations[i].Version;
+ }
+ return matched;
+ }
+
+ ///
+ /// The discriminator column that distinguishes a Brighter outbox/inbox table from any other
+ /// table that happens to share its name.
+ ///
+ public string DiscriminatorFor(BoxType boxType) => boxType switch
+ {
+ BoxType.Outbox => "HeaderBag",
+ BoxType.Inbox => "CommandBody",
+ _ => throw new ArgumentOutOfRangeException(nameof(boxType), boxType, "Unknown BoxType")
+ };
+
+ internal async Task> GetTableColumnsAsHashSetAsync(
+ OracleConnection connection, string tableName, string? schemaName,
+ CancellationToken cancellationToken)
+ {
+ var resolvedSchema = await ResolveSchemaAsync(connection, schemaName, cancellationToken);
+
+ using var command = (OracleCommand)connection.CreateCommand();
+ command.BindByName = true;
+ command.CommandText = @"
+SELECT COLUMN_NAME FROM ALL_TAB_COLUMNS
+WHERE OWNER = UPPER(:SchemaName) AND TABLE_NAME = UPPER(:TableName)";
+ command.Parameters.Add(new OracleParameter("SchemaName", resolvedSchema));
+ command.Parameters.Add(new OracleParameter("TableName", tableName));
+
+ var columns = new HashSet(StringComparer.OrdinalIgnoreCase);
+ using var reader = await command.ExecuteReaderAsync(cancellationToken);
+ while (await reader.ReadAsync(cancellationToken))
+ {
+ columns.Add(reader.GetString(0));
+ }
+ return columns;
+ }
+
+ private static async Task ResolveSchemaAsync(
+ OracleConnection connection, string? schemaName,
+ CancellationToken cancellationToken)
+ {
+ if (schemaName is not null)
+ return schemaName;
+
+ using var command = (OracleCommand)connection.CreateCommand();
+ command.CommandText = "SELECT SYS_CONTEXT('USERENV', 'CURRENT_SCHEMA') FROM DUAL";
+ var result = await command.ExecuteScalarAsync(cancellationToken);
+ return result?.ToString()
+ ?? throw new InvalidOperationException(
+ "Could not resolve the current Oracle schema via SYS_CONTEXT('USERENV','CURRENT_SCHEMA').");
+ }
+}
diff --git a/src/Paramore.Brighter.BoxProvisioning.Oracle/OracleBoxMigrationRunner.cs b/src/Paramore.Brighter.BoxProvisioning.Oracle/OracleBoxMigrationRunner.cs
new file mode 100644
index 0000000000..9ff5b3f930
--- /dev/null
+++ b/src/Paramore.Brighter.BoxProvisioning.Oracle/OracleBoxMigrationRunner.cs
@@ -0,0 +1,270 @@
+// The MIT License (MIT)
+// Copyright © 2026 Ian Cooper
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+using Oracle.ManagedDataAccess.Client;
+using Paramore.Brighter.Logging;
+using Paramore.Brighter.Observability;
+
+namespace Paramore.Brighter.BoxProvisioning.Oracle;
+
+///
+/// Runs box migrations against Oracle. Derives from
+/// for orchestration and
+/// supplies Oracle-specific hooks for connection, advisory lock UoW, history table, and
+/// migration-application paths.
+///
+///
+/// Oracle DDL auto-commits per statement. The runner therefore coordinates concurrency with
+/// and does not rely on a transaction rollback model for DDL.
+///
+public class OracleBoxMigrationRunner : SqlBoxMigrationRunner
+{
+ private const string MIGRATION_HISTORY_TABLE = "BRIGHTER_MIGRATION_HISTORY";
+ private readonly IOracleAdvisoryLock _advisoryLock;
+
+ ///
+ /// Initialises the runner with an explicit detection helper and optional dependencies.
+ ///
+ public OracleBoxMigrationRunner(
+ OracleBoxDetectionHelper detectionHelper,
+ IAmABoxMigrationCatalog catalog,
+ IAmARelationalDatabaseConfiguration configuration,
+ IOracleAdvisoryLock? advisoryLock = null,
+ ILogger? logger = null,
+ TimeSpan? lockTimeout = null,
+ IAmABrighterTracer? tracer = null,
+ MigrationHistoryScope scope = MigrationHistoryScope.Global)
+ : base(detectionHelper, catalog, configuration, lockTimeout ?? TimeSpan.FromSeconds(30),
+ logger ?? ApplicationLogging.CreateLogger(),
+ tracer, scope)
+ {
+ _advisoryLock = advisoryLock ?? new OracleAdvisoryLock();
+ }
+
+ ///
+ /// Convenience ctor used by registration extensions.
+ ///
+ public OracleBoxMigrationRunner(
+ IAmABoxMigrationCatalog catalog,
+ IAmARelationalDatabaseConfiguration configuration,
+ TimeSpan lockTimeout,
+ IOracleAdvisoryLock? advisoryLock = null,
+ ILogger? logger = null,
+ IAmABrighterTracer? tracer = null,
+ MigrationHistoryScope scope = MigrationHistoryScope.Global)
+ : this(new OracleBoxDetectionHelper(), catalog, configuration, advisoryLock, logger, lockTimeout, tracer, scope)
+ {
+ }
+
+ ///
+ protected override DbSystem DbSystem => DbSystem.Oracle;
+
+ ///
+ ///
+ /// Oracle history is stored in the connected user's schema; there is no fixed default
+ /// schema constant for this backend.
+ ///
+ protected override string? DefaultHistorySchema => null;
+
+ protected override async Task OpenConnectionAsync(CancellationToken cancellationToken)
+ {
+ var connection = new OracleConnection(Configuration.ConnectionString);
+ await connection.OpenAsync(cancellationToken);
+ return connection;
+ }
+
+ protected override Task> CreateUnitOfWorkAsync(
+ OracleConnection connection, string? schemaName, string tableName, CancellationToken cancellationToken)
+ {
+ _ = schemaName;
+ _ = cancellationToken;
+ return Task.FromResult>(
+ new OracleProvisioningUnitOfWork(connection, _advisoryLock, Logger, tableName));
+ }
+
+ protected override string LockResourceFor(string? schemaName, string tableName)
+ => OracleMigrationLockName.For(schemaName, tableName);
+
+ protected override async Task EnsureHistoryTableAsync(
+ OracleConnection connection, OracleTransaction? transaction, string? schemaName, string tableName,
+ CancellationToken cancellationToken)
+ {
+ _ = transaction;
+ _ = schemaName;
+ _ = tableName;
+
+ const string ddl = """
+ CREATE TABLE BRIGHTER_MIGRATION_HISTORY (
+ MigrationVersion NUMBER(10) NOT NULL,
+ SchemaName VARCHAR2(256) NOT NULL,
+ BoxTableName VARCHAR2(256) NOT NULL,
+ Description NVARCHAR2(512) NOT NULL,
+ AppliedAt TIMESTAMP WITH TIME ZONE DEFAULT SYSTIMESTAMP NOT NULL,
+ CONSTRAINT PK_BrighterMigrationHistory PRIMARY KEY (SchemaName, BoxTableName, MigrationVersion)
+ )
+ """;
+
+ var escapedDdl = ddl.Replace("'", "''");
+
+ using var command = (OracleCommand)connection.CreateCommand();
+ command.CommandText = $"""
+ BEGIN
+ EXECUTE IMMEDIATE '{escapedDdl}';
+ EXCEPTION
+ WHEN OTHERS THEN
+ IF SQLCODE != -955 THEN
+ RAISE;
+ END IF;
+ END;
+ """;
+
+ await command.ExecuteNonQueryAsync(cancellationToken);
+ }
+
+ protected override async Task RunFreshPathAsync(
+ OracleConnection connection, OracleTransaction? transaction, string? schemaName, string tableName,
+ string freshInstallDdl, int latestVersion, CancellationToken cancellationToken)
+ {
+ _ = transaction;
+ var effectiveSchema = await ResolveSchemaAsync(connection, schemaName, cancellationToken);
+
+ await ExecuteDdlAsync(connection, freshInstallDdl, cancellationToken);
+
+ await InsertHistoryRowAsync(
+ connection, effectiveSchema, tableName,
+ latestVersion, $"fresh install at V{latestVersion}", cancellationToken);
+ }
+
+ protected override async Task RunBootstrapPathAsync(
+ OracleConnection connection, OracleTransaction? transaction, string? schemaName, string tableName,
+ BoxType boxType, IReadOnlyList migrations, CancellationToken cancellationToken)
+ {
+ var effectiveSchema = await ResolveSchemaAsync(connection, schemaName, cancellationToken);
+
+ var detected = await DetectionHelper.DetectCurrentVersionAsync(
+ connection, tableName, effectiveSchema, boxType, migrations, cancellationToken, transaction);
+
+ if (detected == -1)
+ {
+ var discriminator = DetectionHelper.DiscriminatorFor(boxType);
+ throw new ConfigurationException(
+ $"Table '{effectiveSchema}.{tableName}' is not a Brighter {boxType.ToString().ToLowerInvariant()}: " +
+ $"missing discriminator column '{discriminator}'.");
+ }
+
+ if (detected == 0)
+ {
+ throw new ConfigurationException(
+ $"Table '{effectiveSchema}.{tableName}' does not match any known schema version. " +
+ $"Cannot bootstrap a Brighter {boxType.ToString().ToLowerInvariant()} from an unrecognised column set.");
+ }
+
+ await InsertHistoryRowAsync(
+ connection, effectiveSchema, tableName,
+ detected, $"bootstrap: detected at V{detected}", cancellationToken);
+
+ for (var i = 0; i < migrations.Count; i++)
+ {
+ var migration = migrations[i];
+ if (migration.Version <= detected) continue;
+
+ await ExecuteUpScriptAsync(connection, migration, cancellationToken);
+ await InsertHistoryRowAsync(
+ connection, effectiveSchema, tableName,
+ migration.Version, migration.Description.Value, cancellationToken);
+ }
+ }
+
+ protected override async Task RunNormalPathAsync(
+ OracleConnection connection, OracleTransaction? transaction, string? schemaName, string tableName,
+ IReadOnlyList migrations, CancellationToken cancellationToken)
+ {
+ var effectiveSchema = await ResolveSchemaAsync(connection, schemaName, cancellationToken);
+
+ var maxVersion = await DetectionHelper.GetMaxVersionAsync(
+ connection, tableName, effectiveSchema, ResolveHistorySchema(), cancellationToken, transaction);
+
+ foreach (var migration in migrations)
+ {
+ if (migration.Version <= maxVersion) continue;
+
+ await ExecuteUpScriptAsync(connection, migration, cancellationToken);
+ await InsertHistoryRowAsync(
+ connection, effectiveSchema, tableName,
+ migration.Version, migration.Description.Value, cancellationToken);
+ }
+ }
+
+ private static Task ExecuteUpScriptAsync(
+ OracleConnection connection, IAmABoxMigration migration,
+ CancellationToken cancellationToken)
+ => ExecuteDdlAsync(connection, migration.UpScript.Value, cancellationToken);
+
+ private static async Task ExecuteDdlAsync(
+ OracleConnection connection, string ddl,
+ CancellationToken cancellationToken)
+ {
+ using var command = (OracleCommand)connection.CreateCommand();
+ command.BindByName = true;
+ command.CommandText = ddl;
+ await command.ExecuteNonQueryAsync(cancellationToken);
+ }
+
+ private static async Task InsertHistoryRowAsync(
+ OracleConnection connection, string schemaName, string tableName,
+ int version, string description, CancellationToken cancellationToken)
+ {
+ using var command = (OracleCommand)connection.CreateCommand();
+ command.BindByName = true;
+ command.CommandText = """
+ INSERT INTO BRIGHTER_MIGRATION_HISTORY (MigrationVersion, SchemaName, BoxTableName, Description)
+ VALUES (:Version, :SchemaName, :BoxTableName, :Description)
+ """;
+ command.Parameters.Add(new OracleParameter("Version", version));
+ command.Parameters.Add(new OracleParameter("SchemaName", schemaName));
+ command.Parameters.Add(new OracleParameter("BoxTableName", tableName));
+ command.Parameters.Add(new OracleParameter("Description", description));
+
+ await command.ExecuteNonQueryAsync(cancellationToken);
+ }
+
+ private static async Task ResolveSchemaAsync(
+ OracleConnection connection, string? schemaName,
+ CancellationToken cancellationToken)
+ {
+ if (schemaName is not null)
+ {
+ return schemaName;
+ }
+
+ using var command = (OracleCommand)connection.CreateCommand();
+ command.CommandText = "SELECT SYS_CONTEXT('USERENV', 'CURRENT_SCHEMA') FROM DUAL";
+ var result = await command.ExecuteScalarAsync(cancellationToken);
+ return result?.ToString()
+ ?? throw new InvalidOperationException(
+ "Could not resolve the current Oracle schema via SYS_CONTEXT('USERENV','CURRENT_SCHEMA').");
+ }
+}
diff --git a/src/Paramore.Brighter.BoxProvisioning.Oracle/OracleBoxProvisioningExtensions.cs b/src/Paramore.Brighter.BoxProvisioning.Oracle/OracleBoxProvisioningExtensions.cs
new file mode 100644
index 0000000000..bdca9e9fd4
--- /dev/null
+++ b/src/Paramore.Brighter.BoxProvisioning.Oracle/OracleBoxProvisioningExtensions.cs
@@ -0,0 +1,172 @@
+// The MIT License (MIT)
+// Copyright © 2026 Ian Cooper
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using Paramore.Brighter.Observability;
+
+namespace Paramore.Brighter.BoxProvisioning.Oracle;
+
+///
+/// Extension methods for registering Oracle box provisioners with .
+///
+public static class OracleBoxProvisioningExtensions
+{
+ ///
+ /// Register Oracle outbox provisioning with an explicit configuration.
+ ///
+ public static BoxProvisioningOptions AddOracleOutbox(
+ this BoxProvisioningOptions options,
+ IAmARelationalDatabaseConfiguration configuration)
+ {
+ options.Add(services =>
+ {
+ RegisterSharedRoleImpls(services);
+ services.TryAddSingleton();
+ services.AddSingleton(sp =>
+ {
+ var catalog = sp.GetRequiredService();
+ var runner = new OracleBoxMigrationRunner(catalog, configuration, options.MigrationLockTimeout,
+ tracer: sp.GetService(), scope: options.MigrationHistoryScope);
+ return new OracleOutboxProvisioner(
+ sp.GetRequiredService(),
+ catalog,
+ sp.GetRequiredService(),
+ configuration,
+ runner);
+ });
+ });
+ return options;
+ }
+
+ ///
+ /// Register Oracle outbox provisioning with a connection name resolved from IConfiguration at runtime.
+ ///
+ public static BoxProvisioningOptions AddOracleOutbox(
+ this BoxProvisioningOptions options,
+ string connectionName,
+ string? outboxTableName = null,
+ string? schemaName = null,
+ bool binaryMessagePayload = false)
+ {
+ options.Add(services =>
+ {
+ RegisterSharedRoleImpls(services);
+ services.TryAddSingleton();
+ services.AddSingleton(sp =>
+ {
+ var config = sp.GetRequiredService();
+ var connectionString = config.GetConnectionString(connectionName)
+ ?? throw new InvalidOperationException(
+ $"Connection string '{connectionName}' not found in configuration.");
+ var dbConfig = new RelationalDatabaseConfiguration(
+ connectionString,
+ outBoxTableName: outboxTableName ?? "Outbox",
+ schemaName: schemaName,
+ binaryMessagePayload: binaryMessagePayload);
+ var catalog = sp.GetRequiredService();
+ var runner = new OracleBoxMigrationRunner(catalog, dbConfig, options.MigrationLockTimeout,
+ tracer: sp.GetService(), scope: options.MigrationHistoryScope);
+ return new OracleOutboxProvisioner(
+ sp.GetRequiredService(),
+ catalog,
+ sp.GetRequiredService(),
+ dbConfig,
+ runner);
+ });
+ });
+ return options;
+ }
+
+ ///
+ /// Register Oracle inbox provisioning with an explicit configuration.
+ ///
+ public static BoxProvisioningOptions AddOracleInbox(
+ this BoxProvisioningOptions options,
+ IAmARelationalDatabaseConfiguration configuration)
+ {
+ options.Add(services =>
+ {
+ RegisterSharedRoleImpls(services);
+ services.TryAddSingleton();
+ services.AddSingleton(sp =>
+ {
+ var catalog = sp.GetRequiredService();
+ var runner = new OracleBoxMigrationRunner(catalog, configuration, options.MigrationLockTimeout,
+ tracer: sp.GetService(), scope: options.MigrationHistoryScope);
+ return new OracleInboxProvisioner(
+ sp.GetRequiredService(),
+ catalog,
+ sp.GetRequiredService(),
+ configuration,
+ runner);
+ });
+ });
+ return options;
+ }
+
+ ///
+ /// Register Oracle inbox provisioning with a connection name resolved from IConfiguration at runtime.
+ ///
+ public static BoxProvisioningOptions AddOracleInbox(
+ this BoxProvisioningOptions options,
+ string connectionName,
+ string? inboxTableName = null,
+ string? schemaName = null,
+ bool binaryMessagePayload = false)
+ {
+ options.Add(services =>
+ {
+ RegisterSharedRoleImpls(services);
+ services.TryAddSingleton();
+ services.AddSingleton(sp =>
+ {
+ var config = sp.GetRequiredService();
+ var connectionString = config.GetConnectionString(connectionName)
+ ?? throw new InvalidOperationException(
+ $"Connection string '{connectionName}' not found in configuration.");
+ var dbConfig = new RelationalDatabaseConfiguration(
+ connectionString,
+ inboxTableName: inboxTableName ?? "Inbox",
+ schemaName: schemaName,
+ binaryMessagePayload: binaryMessagePayload);
+ var catalog = sp.GetRequiredService();
+ var runner = new OracleBoxMigrationRunner(catalog, dbConfig, options.MigrationLockTimeout,
+ tracer: sp.GetService(), scope: options.MigrationHistoryScope);
+ return new OracleInboxProvisioner(
+ sp.GetRequiredService(),
+ catalog,
+ sp.GetRequiredService(),
+ dbConfig,
+ runner);
+ });
+ });
+ return options;
+ }
+
+ private static void RegisterSharedRoleImpls(IServiceCollection services)
+ {
+ services.TryAddSingleton();
+ services.TryAddSingleton();
+ }
+}
diff --git a/src/Paramore.Brighter.BoxProvisioning.Oracle/OracleInboxMigrationCatalog.cs b/src/Paramore.Brighter.BoxProvisioning.Oracle/OracleInboxMigrationCatalog.cs
new file mode 100644
index 0000000000..e4794496a3
--- /dev/null
+++ b/src/Paramore.Brighter.BoxProvisioning.Oracle/OracleInboxMigrationCatalog.cs
@@ -0,0 +1,79 @@
+// The MIT License (MIT)
+// Copyright © 2026 Ian Cooper
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System.Collections.Generic;
+using Paramore.Brighter.Inbox.Oracle;
+
+namespace Paramore.Brighter.BoxProvisioning.Oracle;
+
+///
+/// Defines the migration history for Oracle inbox tables.
+///
+///
+/// The Oracle inbox was introduced with the full current column set from the start (V1 =
+/// V_latest). V1's UpScript is the full current DDL. The bootstrap detection branch
+/// identifies pre-BoxProvisioning Oracle inbox tables (created directly via
+/// ) as V1 because the V1 LogicalColumns set
+/// equals the columns the builder always produced.
+///
+/// LogicalColumns use — Oracle stores
+/// column names as uppercase in ALL_TAB_COLUMNS but the comparison must handle
+/// PascalCase names from the migration definitions.
+///
+///
+public class OracleInboxMigrationCatalog : IAmABoxMigrationCatalog
+{
+ // Oracle inbox was born with all columns present; V1 LogicalColumns = full set.
+ private static readonly string[] s_v1Columns =
+ ["CommandId", "CommandType", "CommandBody", "Timestamp", "ContextKey"];
+
+ ///
+ public string FreshInstallDdl(IAmARelationalDatabaseConfiguration configuration)
+ {
+ Identifiers.AssertSafe(
+ configuration.InBoxTableName,
+ nameof(IAmARelationalDatabaseConfiguration.InBoxTableName));
+ return OracleInboxBuilder.GetDDL(
+ configuration.InBoxTableName,
+ configuration.BinaryMessagePayload);
+ }
+
+ ///
+ /// Returns all migrations for the Oracle inbox, ordered by version.
+ ///
+ /// The relational database configuration.
+ /// An ordered list containing V1 (the full column set — born at V_latest).
+ public IReadOnlyList All(IAmARelationalDatabaseConfiguration configuration)
+ {
+ var table = configuration.InBoxTableName;
+ Identifiers.AssertSafe(table, nameof(IAmARelationalDatabaseConfiguration.InBoxTableName));
+
+ return
+ [
+ new BoxMigration(
+ Version: 1,
+ Description: "Create inbox table (Oracle born at full column set)",
+ UpScript: OracleInboxBuilder.GetDDL(table, configuration.BinaryMessagePayload),
+ LogicalColumns: new System.Collections.Generic.HashSet(
+ s_v1Columns, System.StringComparer.OrdinalIgnoreCase))
+ ];
+ }
+}
diff --git a/src/Paramore.Brighter.BoxProvisioning.Oracle/OracleInboxProvisioner.cs b/src/Paramore.Brighter.BoxProvisioning.Oracle/OracleInboxProvisioner.cs
new file mode 100644
index 0000000000..b76c3715ce
--- /dev/null
+++ b/src/Paramore.Brighter.BoxProvisioning.Oracle/OracleInboxProvisioner.cs
@@ -0,0 +1,52 @@
+// The MIT License (MIT)
+// Copyright © 2026 Ian Cooper
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using Oracle.ManagedDataAccess.Client;
+
+namespace Paramore.Brighter.BoxProvisioning.Oracle;
+
+///
+/// Provisions an Oracle inbox table. Pre-lock detection and payload-mode validation are owned
+/// by the base; this class supplies
+/// only the abstract hooks for the Oracle connection factory and the inbox payload column name.
+///
+public class OracleInboxProvisioner : SqlBoxProvisioner
+{
+ ///
+ /// Initialises the provisioner with all required dependencies.
+ ///
+ public OracleInboxProvisioner(
+ IAmAVersionDetectingMigrationHelper detectionHelper,
+ IAmABoxMigrationCatalog catalog,
+ IAmABoxPayloadModeValidator payloadValidator,
+ IAmARelationalDatabaseConfiguration configuration,
+ IAmABoxMigrationRunner migrationRunner)
+ : base(detectionHelper, catalog, payloadValidator, configuration, migrationRunner, BoxType.Inbox)
+ {
+ }
+
+ ///
+ protected override OracleConnection CreateConnection(string connectionString)
+ => new OracleConnection(connectionString);
+
+ ///
+ protected override string PayloadColumnName => "CommandBody";
+}
diff --git a/src/Paramore.Brighter.BoxProvisioning.Oracle/OracleMigrationLockName.cs b/src/Paramore.Brighter.BoxProvisioning.Oracle/OracleMigrationLockName.cs
new file mode 100644
index 0000000000..cf6005b31f
--- /dev/null
+++ b/src/Paramore.Brighter.BoxProvisioning.Oracle/OracleMigrationLockName.cs
@@ -0,0 +1,92 @@
+// The MIT License (MIT)
+// Copyright © 2026 Ian Cooper
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System.Security.Cryptography;
+using System.Text;
+
+namespace Paramore.Brighter.BoxProvisioning.Oracle;
+
+///
+/// Builds the advisory-lock identifier passed to DBMS_LOCK.ALLOCATE_UNIQUE for a
+/// given (schema, table) pair.
+///
+///
+/// DBMS_LOCK.ALLOCATE_UNIQUE supports lock names up to 128 characters.
+/// The schema is folded into the lock name so two same-named tables in different schemas
+/// acquire distinct advisory locks.
+///
+/// For composites that fit the simple form
+/// (BrighterMigration_<schema>.<table> ≤ 128 chars, or
+/// BrighterMigration_<table> when schema is null/empty) the form is preserved
+/// for diagnostic readability. For longer composites a SHA-256 suffix is folded in to stay
+/// within the limit while remaining collision-resistant.
+///
+///
+/// Hash truncation: the suffix is the first 8 bytes (16 hex characters) of the SHA-256
+/// digest. Birthday-bound collision probability is ~1 in 2^32 over distinct (schema, table)
+/// pairs — accepted as negligible for typical per-deployment box-table counts.
+///
+///
+public static class OracleMigrationLockName
+{
+ private const string Prefix = "BrighterMigration_";
+ private const int DbmsLockNameLimit = 128;
+ private const int HashHexChars = 16;
+ // Long-form layout: Prefix(18) + truncatedPrefix(N) + '_'(1) + hashHex(16) ≤ 128 → N ≤ 93.
+ private const int LongFormPrefixBudget = 93;
+
+ ///
+ /// Builds the lock name for the given and ,
+ /// guaranteed not to exceed the 128-character DBMS_LOCK.ALLOCATE_UNIQUE limit.
+ ///
+ /// The Oracle schema the table lives in. When null or empty the schema
+ /// is omitted; the runner always passes the resolved current schema.
+ /// The box table name.
+ /// A lock identifier suitable for DBMS_LOCK.ALLOCATE_UNIQUE.
+ public static string For(string? schema, string tableName)
+ {
+ var composite = string.IsNullOrEmpty(schema)
+ ? tableName
+ : $"{schema}.{tableName}";
+
+ var simpleForm = Prefix + composite;
+ if (simpleForm.Length <= DbmsLockNameLimit)
+ return simpleForm;
+
+ var hashSuffix = ShortHashOf(composite);
+ var truncatedPrefix = composite.Substring(0, LongFormPrefixBudget);
+ return $"{Prefix}{truncatedPrefix}_{hashSuffix}";
+ }
+
+ private static string ShortHashOf(string composite)
+ {
+ using var sha256 = SHA256.Create();
+ var hashBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(composite));
+
+ var builder = new StringBuilder(HashHexChars);
+ for (var i = 0; i < HashHexChars / 2; i++)
+ {
+ builder.Append(hashBytes[i].ToString("x2"));
+ }
+
+ return builder.ToString();
+ }
+}
diff --git a/src/Paramore.Brighter.BoxProvisioning.Oracle/OracleOutboxMigrationCatalog.cs b/src/Paramore.Brighter.BoxProvisioning.Oracle/OracleOutboxMigrationCatalog.cs
new file mode 100644
index 0000000000..84825bac41
--- /dev/null
+++ b/src/Paramore.Brighter.BoxProvisioning.Oracle/OracleOutboxMigrationCatalog.cs
@@ -0,0 +1,92 @@
+// The MIT License (MIT)
+// Copyright © 2026 Ian Cooper
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System.Collections.Generic;
+using Paramore.Brighter.Outbox.Oracle;
+
+namespace Paramore.Brighter.BoxProvisioning.Oracle;
+
+///
+/// Defines the migration history for Oracle outbox tables.
+///
+///
+/// The Oracle outbox was introduced with the full current column set from the start (V1 =
+/// V_latest). Unlike MySQL whose V1 carried only the March-2019 minimal shape and accumulated
+/// columns through V7, the Oracle provider was never shipped with a subset of these columns.
+///
+/// V1's UpScript is therefore the full current DDL — the same as .
+/// The fresh-install fast path (ADR 0057 §3) still sources its DDL from
+/// so the bootstrap and fresh-install paths are consistent. The detection bootstrap branch
+/// identifies pre-BoxProvisioning Oracle tables (created directly via
+/// ) as V1 because the V1 LogicalColumns set
+/// is a subset of (or equal to) every column the builder ever produced.
+///
+///
+/// LogicalColumns use — Oracle stores
+/// column names as uppercase in ALL_TAB_COLUMNS but the comparison must handle
+/// PascalCase names from the migration definitions.
+///
+///
+public class OracleOutboxMigrationCatalog : IAmABoxMigrationCatalog
+{
+ // Oracle outbox was born with all columns present; V1 LogicalColumns = full set.
+ private static readonly string[] s_v1Columns =
+ [
+ "MessageId", "Topic", "MessageType", "Timestamp",
+ "CorrelationId", "ReplyTo", "ContentType", "PartitionKey",
+ "WorkflowId", "JobId", "Dispatched", "HeaderBag", "Body",
+ "Source", "Type", "DataSchema", "Subject",
+ "TraceParent", "TraceState", "Baggage",
+ "DataRef", "SpecVersion"
+ ];
+
+ ///
+ public string FreshInstallDdl(IAmARelationalDatabaseConfiguration configuration)
+ {
+ Identifiers.AssertSafe(
+ configuration.OutBoxTableName,
+ nameof(IAmARelationalDatabaseConfiguration.OutBoxTableName));
+ return OracleOutboxBuilder.GetDDL(
+ configuration.OutBoxTableName,
+ configuration.BinaryMessagePayload);
+ }
+
+ ///
+ /// Returns all migrations for the Oracle outbox, ordered by version.
+ ///
+ /// The relational database configuration.
+ /// An ordered list containing V1 (the full column set — born at V_latest).
+ public IReadOnlyList All(IAmARelationalDatabaseConfiguration configuration)
+ {
+ var table = configuration.OutBoxTableName;
+ Identifiers.AssertSafe(table, nameof(IAmARelationalDatabaseConfiguration.OutBoxTableName));
+
+ return
+ [
+ new BoxMigration(
+ Version: 1,
+ Description: "Create outbox table (Oracle born at full column set)",
+ UpScript: OracleOutboxBuilder.GetDDL(table, configuration.BinaryMessagePayload),
+ LogicalColumns: new System.Collections.Generic.HashSet(
+ s_v1Columns, System.StringComparer.OrdinalIgnoreCase))
+ ];
+ }
+}
diff --git a/src/Paramore.Brighter.BoxProvisioning.Oracle/OracleOutboxProvisioner.cs b/src/Paramore.Brighter.BoxProvisioning.Oracle/OracleOutboxProvisioner.cs
new file mode 100644
index 0000000000..cb01b2c281
--- /dev/null
+++ b/src/Paramore.Brighter.BoxProvisioning.Oracle/OracleOutboxProvisioner.cs
@@ -0,0 +1,52 @@
+// The MIT License (MIT)
+// Copyright © 2026 Ian Cooper
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using Oracle.ManagedDataAccess.Client;
+
+namespace Paramore.Brighter.BoxProvisioning.Oracle;
+
+///
+/// Provisions an Oracle outbox table. Pre-lock detection and payload-mode validation are owned
+/// by the base; this class supplies
+/// only the abstract hooks for the Oracle connection factory and the outbox payload column name.
+///
+public class OracleOutboxProvisioner : SqlBoxProvisioner
+{
+ ///
+ /// Initialises the provisioner with all required dependencies.
+ ///
+ public OracleOutboxProvisioner(
+ IAmAVersionDetectingMigrationHelper detectionHelper,
+ IAmABoxMigrationCatalog catalog,
+ IAmABoxPayloadModeValidator payloadValidator,
+ IAmARelationalDatabaseConfiguration configuration,
+ IAmABoxMigrationRunner migrationRunner)
+ : base(detectionHelper, catalog, payloadValidator, configuration, migrationRunner, BoxType.Outbox)
+ {
+ }
+
+ ///
+ protected override OracleConnection CreateConnection(string connectionString)
+ => new OracleConnection(connectionString);
+
+ ///
+ protected override string PayloadColumnName => "Body";
+}
diff --git a/src/Paramore.Brighter.BoxProvisioning.Oracle/OraclePayloadModeValidator.cs b/src/Paramore.Brighter.BoxProvisioning.Oracle/OraclePayloadModeValidator.cs
new file mode 100644
index 0000000000..bdb6c66c17
--- /dev/null
+++ b/src/Paramore.Brighter.BoxProvisioning.Oracle/OraclePayloadModeValidator.cs
@@ -0,0 +1,107 @@
+// The MIT License (MIT)
+// Copyright © 2026 Ian Cooper
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using Oracle.ManagedDataAccess.Client;
+
+namespace Paramore.Brighter.BoxProvisioning.Oracle;
+
+///
+/// Validates that the payload column type in an existing Oracle box table matches the
+/// configured binary/text mode. Throws on mismatch
+/// and returns quietly on match.
+///
+///
+/// Queries ALL_TAB_COLUMNS for the column's DATA_TYPE. Oracle stores column
+/// names and owner names in uppercase by default; identifiers are uppercased before the
+/// lookup so callers may pass mixed-case names without issue.
+/// When is null the current schema is resolved via
+/// SYS_CONTEXT('USERENV','CURRENT_SCHEMA').
+/// Stateless service; safe to register as a DI singleton.
+///
+public class OraclePayloadModeValidator : IAmABoxPayloadModeValidator
+{
+ ///
+ /// Validates the payload mode of an existing table column against ALL_TAB_COLUMNS.
+ ///
+ /// An open Oracle connection.
+ /// The unqualified box table name.
+ ///
+ /// Optional. Null is resolved to SYS_CONTEXT('USERENV','CURRENT_SCHEMA').
+ ///
+ /// The payload column name (e.g. "Body" or "CommandBody").
+ /// Whether binary payload mode is configured.
+ /// Cancellation token.
+ public async Task ValidateAsync(
+ OracleConnection connection, string tableName, string? schemaName,
+ string columnName, bool binaryMessagePayload,
+ CancellationToken cancellationToken = default)
+ {
+ var resolvedSchema = schemaName is not null
+ ? schemaName
+ : await ResolveCurrentSchemaAsync(connection, cancellationToken);
+
+ using var command = (OracleCommand)connection.CreateCommand();
+ command.BindByName = true;
+ command.CommandText = @"
+SELECT DATA_TYPE FROM ALL_TAB_COLUMNS
+WHERE OWNER = UPPER(:SchemaName) AND TABLE_NAME = UPPER(:TableName) AND COLUMN_NAME = UPPER(:ColumnName)";
+ command.Parameters.Add(new OracleParameter("SchemaName", resolvedSchema));
+ command.Parameters.Add(new OracleParameter("TableName", tableName));
+ command.Parameters.Add(new OracleParameter("ColumnName", columnName));
+
+ var dataType = (string?)await command.ExecuteScalarAsync(cancellationToken);
+ if (dataType == null) return;
+
+ var isBinary = dataType.Equals("BLOB", StringComparison.OrdinalIgnoreCase);
+ var isText = dataType.Equals("CLOB", StringComparison.OrdinalIgnoreCase)
+ || dataType.Equals("NCLOB", StringComparison.OrdinalIgnoreCase);
+
+ if (binaryMessagePayload && isText)
+ {
+ throw new ConfigurationException(
+ $"Payload mode mismatch for table \"{resolvedSchema}\".\"{tableName}\". " +
+ $"Column \"{columnName}\" is {dataType} (text) but binary mode was configured. " +
+ "Either change the configuration or alter the column type.");
+ }
+
+ if (!binaryMessagePayload && isBinary)
+ {
+ throw new ConfigurationException(
+ $"Payload mode mismatch for table \"{resolvedSchema}\".\"{tableName}\". " +
+ $"Column \"{columnName}\" is {dataType} (binary) but text mode was configured. " +
+ "Either change the configuration or alter the column type.");
+ }
+ }
+
+ private static async Task ResolveCurrentSchemaAsync(
+ OracleConnection connection, CancellationToken cancellationToken)
+ {
+ using var command = (OracleCommand)connection.CreateCommand();
+ command.CommandText = "SELECT SYS_CONTEXT('USERENV', 'CURRENT_SCHEMA') FROM DUAL";
+ var result = await command.ExecuteScalarAsync(cancellationToken);
+ return result?.ToString()
+ ?? throw new InvalidOperationException(
+ "Could not resolve the current Oracle schema via SYS_CONTEXT('USERENV','CURRENT_SCHEMA').");
+ }
+}
diff --git a/src/Paramore.Brighter.BoxProvisioning.Oracle/OracleProvisioningUnitOfWork.cs b/src/Paramore.Brighter.BoxProvisioning.Oracle/OracleProvisioningUnitOfWork.cs
new file mode 100644
index 0000000000..e83928b3ae
--- /dev/null
+++ b/src/Paramore.Brighter.BoxProvisioning.Oracle/OracleProvisioningUnitOfWork.cs
@@ -0,0 +1,122 @@
+// The MIT License (MIT)
+// Copyright © 2026 Ian Cooper
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+using Oracle.ManagedDataAccess.Client;
+
+namespace Paramore.Brighter.BoxProvisioning.Oracle;
+
+///
+/// Oracle unit-of-work for box-table migrations. Oracle is the transactionless member of the
+/// relational family for migration purposes — DDL implicitly commits the surrounding transaction,
+/// so the UoW only acquires the session-level DBMS_LOCK that serialises concurrent
+/// runners and never opens a ; the
+/// property is always null.
+///
+///
+/// The class implements with
+/// TTransaction = OracleTransaction for symmetry with the other relational backends —
+/// see the XML-doc which
+/// documents the null-Transaction contract for transactionless backends.
+///
+public class OracleProvisioningUnitOfWork(
+ OracleConnection connection,
+ IOracleAdvisoryLock advisoryLock,
+ ILogger logger,
+ string? tableName = null) : IAmAProvisioningUnitOfWork
+{
+ private string? _lockResource;
+
+ ///
+ ///
+ /// Always returns null — Oracle DDL auto-commits per statement.
+ ///
+ public OracleTransaction? Transaction => null;
+
+ ///
+ public async Task BeginAsync(string lockResource, TimeSpan lockTimeout, CancellationToken cancellationToken)
+ {
+ // Oracle DDL implicitly commits, so wrapping the migration run in a BEGIN/COMMIT yields
+ // nothing useful. The UoW therefore only acquires the DBMS_LOCK advisory lock and stores
+ // the lock resource for explicit RELEASE on Commit/Rollback. No transaction is opened.
+ //
+ // _lockResource is assigned ONLY AFTER AcquireAsync returns successfully. If Acquire
+ // throws, _lockResource remains null so a defensive Rollback is a clean no-op via the
+ // `if (_lockResource is null) return;` short-circuit in ReleaseLockAsync.
+ logger.LogTrace("Beginning Oracle provisioning UoW for resource {LockResource}", lockResource);
+ await advisoryLock.AcquireAsync(connection, lockResource, lockTimeout, cancellationToken);
+ _lockResource = lockResource;
+ }
+
+ ///
+ public Task CommitAsync(CancellationToken cancellationToken) =>
+ ReleaseLockAsync(cancellationToken);
+
+ ///
+ public Task RollbackAsync(CancellationToken cancellationToken) =>
+ ReleaseLockAsync(cancellationToken);
+
+ private async Task ReleaseLockAsync(CancellationToken cancellationToken)
+ {
+ if (_lockResource is null) return;
+
+ bool? releaseResult;
+ try
+ {
+ releaseResult = await advisoryLock.ReleaseAsync(connection, _lockResource, cancellationToken);
+ }
+ catch (Exception ex)
+ {
+ // DBMS_LOCK.RELEASE executes SQL on the same connection. If the connection is dead,
+ // ReleaseAsync throws. RollbackAsync MUST NOT throw — otherwise the runner's catch
+ // path replaces the original migration exception with this cleanup-side failure.
+ // The session-scoped DBMS_LOCK is released by the server when the connection closes.
+ logger.LogWarning(
+ ex,
+ "Oracle provisioning UoW: DBMS_LOCK.RELEASE threw for lock resource '{LockResource}' — release skipped, the session will release the lock when the connection closes.",
+ _lockResource);
+ return;
+ }
+
+ if (releaseResult is true) return;
+
+ var marker = releaseResult is null ? "null" : "false";
+ var meaning = releaseResult is null ? "lock was never acquired" : "not the lock owner or parameter error";
+ if (tableName is not null)
+ {
+ logger.LogWarning(
+ "Oracle advisory lock for migration of '{TableName}' (key '{LockKey}') was not released by this session: DBMS_LOCK.RELEASE returned {Result} ({Marker} = {Meaning}). This is likely a Brighter defect — please report it.",
+ tableName, _lockResource, releaseResult, marker, meaning);
+ }
+ else
+ {
+ logger.LogWarning(
+ "Oracle advisory lock '{LockResource}' was not released by this session: DBMS_LOCK.RELEASE returned {Result} ({Marker} = {Meaning}). This is likely a Brighter defect — please report it.",
+ _lockResource, releaseResult, marker, meaning);
+ }
+ }
+
+ ///
+ public ValueTask DisposeAsync() => default;
+}
diff --git a/src/Paramore.Brighter.BoxProvisioning.Oracle/Paramore.Brighter.BoxProvisioning.Oracle.csproj b/src/Paramore.Brighter.BoxProvisioning.Oracle/Paramore.Brighter.BoxProvisioning.Oracle.csproj
new file mode 100644
index 0000000000..4ba1542b0f
--- /dev/null
+++ b/src/Paramore.Brighter.BoxProvisioning.Oracle/Paramore.Brighter.BoxProvisioning.Oracle.csproj
@@ -0,0 +1,27 @@
+
+
+
+ Oracle Database box provisioning for Paramore.Brighter. Provides automatic creation and migration of inbox and outbox tables in Oracle at application startup.
+ Rafael Andrade
+ $(BrighterMongoTargetFrameworks)
+ Oracle;Oracle Database;Database;Outbox;Inbox;Provisioning;Migration;Command;Event;Command Processor;Messaging;Brighter
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Paramore.Brighter.Inbox.Oracle/OracleInbox.cs b/src/Paramore.Brighter.Inbox.Oracle/OracleInbox.cs
new file mode 100644
index 0000000000..3e91e29a8b
--- /dev/null
+++ b/src/Paramore.Brighter.Inbox.Oracle/OracleInbox.cs
@@ -0,0 +1,104 @@
+// The MIT License (MIT)
+// Copyright © 2014 Ian Cooper
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+using System.Data;
+using System.Data.Common;
+using Oracle.ManagedDataAccess.Client;
+using Paramore.Brighter.Logging;
+using Paramore.Brighter.Observability;
+using Paramore.Brighter.Oracle;
+
+namespace Paramore.Brighter.Inbox.Oracle;
+
+///
+/// Implements an Inbox using Oracle as a backing store.
+/// Requires Oracle 12c or later.
+///
+public class OracleInbox : RelationalDatabaseInbox
+{
+ // ORA-00001: unique constraint violated
+ private const int DuplicateKeyError = 1;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The relational database configuration.
+ /// The connection provider.
+ public OracleInbox(IAmARelationalDatabaseConfiguration configuration,
+ IAmARelationalDbConnectionProvider connectionProvider)
+ : base(DbSystem.Oracle, configuration, connectionProvider, new OracleQueries(),
+ ApplicationLogging.CreateLogger())
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class using a default connection provider.
+ ///
+ /// The relational database configuration.
+ public OracleInbox(IAmARelationalDatabaseConfiguration configuration)
+ : this(configuration, new OracleConnectionProvider(configuration))
+ {
+ }
+
+ ///
+ protected override bool IsExceptionUniqueOrDuplicateIssue(Exception ex)
+ {
+ return ex is OracleException { Number: DuplicateKeyError };
+ }
+
+ ///
+ protected override IDbDataParameter CreateSqlParameter(string parameterName, object? value)
+ {
+ parameterName = parameterName.Replace("@", ":");
+ return new OracleParameter { ParameterName = parameterName, Value = value ?? DBNull.Value };
+ }
+
+ ///
+ ///
+ /// Uses rather than OracleDbType.Json because
+ /// the native Oracle JSON type is only available from Oracle Database 21c onwards.
+ /// Oracle 19c stores JSON as a CLOB column validated by an IS JSON check constraint.
+ ///
+ protected override IDbDataParameter CreateJsonSqlParameter(string parameterName, object? value)
+ {
+ parameterName = parameterName.Replace("@", ":");
+ return new OracleParameter
+ {
+ ParameterName = parameterName, Value = value ?? DBNull.Value, OracleDbType = OracleDbType.Clob
+ };
+ }
+
+ ///
+ /// Sets to so that
+ /// named :param placeholders in the SQL are matched to parameters by name.
+ protected override DbCommand CreateCommand(DbConnection connection, string sqlText, int outBoxTimeout,
+ params IDbDataParameter[] parameters)
+ {
+ var command = (OracleCommand)connection.CreateCommand();
+ command.BindByName = true;
+ command.CommandTimeout = outBoxTimeout < 0 ? 0 : outBoxTimeout;
+ command.CommandText = sqlText;
+ command.Parameters.AddRange(parameters);
+ return command;
+ }
+
+}
diff --git a/src/Paramore.Brighter.Inbox.Oracle/OracleInboxBuilder.cs b/src/Paramore.Brighter.Inbox.Oracle/OracleInboxBuilder.cs
new file mode 100644
index 0000000000..30d1fe86cf
--- /dev/null
+++ b/src/Paramore.Brighter.Inbox.Oracle/OracleInboxBuilder.cs
@@ -0,0 +1,126 @@
+// The MIT License (MIT)
+// Copyright © 2014 Ian Cooper
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System.ComponentModel;
+
+namespace Paramore.Brighter.Inbox.Oracle;
+
+///
+/// Provides DDL statements for creating and checking the existence of an Inbox table in Oracle.
+/// Requires Oracle 12c or later (uses identity columns and TIMESTAMP WITH TIME ZONE).
+///
+public class OracleInboxBuilder
+{
+ private const string TextInboxDDL =
+ """
+ CREATE TABLE {0}
+ (
+ Id NUMBER(19) GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
+ CommandId VARCHAR2(256 CHAR) NOT NULL,
+ CommandType VARCHAR2(256 CHAR),
+ CommandBody CLOB,
+ Timestamp TIMESTAMP WITH TIME ZONE,
+ ContextKey VARCHAR2(256 CHAR),
+ CONSTRAINT UQ_{0}_Command_Context UNIQUE (CommandId, ContextKey) -- Optional: keeps the unique combo constraint if needed
+ );
+ """;
+
+ private const string BinaryInboxDDL =
+ """
+ CREATE TABLE {0}
+ (
+ Id NUMBER(19) GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
+ CommandId VARCHAR2(256 CHAR) NOT NULL,
+ CommandType VARCHAR2(256 CHAR),
+ CommandBody BLOB,
+ Timestamp TIMESTAMP WITH TIME ZONE,
+ ContextKey VARCHAR2(256 CHAR),
+ CONSTRAINT UQ_{0}_Command_Context UNIQUE (CommandId, ContextKey) -- Optional: keeps the unique combo constraint if needed
+ );
+ """;
+
+ private const string JsonInboxDDL =
+ """
+ CREATE TABLE {0}
+ (
+ Id NUMBER(19) GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
+ CommandId VARCHAR2(256 CHAR) NOT NULL,
+ CommandType VARCHAR2(256 CHAR),
+ CommandBody BLOB,
+ Timestamp TIMESTAMP WITH TIME ZONE,
+ ContextKey VARCHAR2(256 CHAR),
+ CONSTRAINT UQ_{0}_Command_Context UNIQUE (CommandId, ContextKey),
+ CONSTRAINT ENSURE_{0}_JSON CHECK (CommandBody IS JSON)
+ );
+ """;
+
+ private const string InboxExistsQuery =
+ """
+ SELECT COUNT(1)
+ FROM ALL_TABLES
+ WHERE OWNER = '{0}'
+ AND TABLE_NAME = '{1}';
+ """;
+
+
+ ///
+ /// Gets the DDL statement required to create the Inbox table in Oracle.
+ ///
+ /// The name to give the Inbox table.
+ ///
+ /// When , the CommandBody column is created as BLOB.
+ /// Ignored when is .
+ ///
+ ///
+ /// When , the CommandBody column is created as BLOB
+ /// with an IS JSON check constraint. Requires Oracle 12c or later.
+ /// Takes precedence over .
+ ///
+ /// A CREATE TABLE statement formatted with the given table name.
+ public static string GetDDL(string inboxTableName, bool binaryMessage = false, bool jsonMessage = false)
+ {
+ if (jsonMessage)
+ {
+ return string.Format(JsonInboxDDL, inboxTableName);
+ }
+
+ if (binaryMessage)
+ {
+ return string.Format(BinaryInboxDDL, inboxTableName);
+ }
+
+ return string.Format(TextInboxDDL, inboxTableName);
+ }
+
+ ///
+ /// Gets the SQL statement that checks whether the Inbox table already exists in the given schema.
+ ///
+ /// The Oracle schema (owner) to search within.
+ /// The table name to check.
+ /// A SQL query that returns 1 if the table exists, 0 otherwise.
+ /// Thrown when or is null or empty.
+ public static string GetExistsQuery(string ownerName, string inboxTableName)
+ {
+ if (string.IsNullOrEmpty(ownerName) || string.IsNullOrEmpty(inboxTableName))
+ throw new InvalidEnumArgumentException("You must provide a table schema and tablename for the inbox table");
+ return string.Format(InboxExistsQuery, ownerName, inboxTableName);
+ }
+}
diff --git a/src/Paramore.Brighter.Inbox.Oracle/OracleQueries.cs b/src/Paramore.Brighter.Inbox.Oracle/OracleQueries.cs
new file mode 100644
index 0000000000..1258efeffd
--- /dev/null
+++ b/src/Paramore.Brighter.Inbox.Oracle/OracleQueries.cs
@@ -0,0 +1,44 @@
+// The MIT License (MIT)
+// Copyright © 2014 Ian Cooper
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+namespace Paramore.Brighter.Inbox.Oracle;
+
+///
+/// Oracle-specific SQL queries for the Brighter Inbox.
+/// Uses named bind variables prefixed with :; parameter names are matched by name
+/// via .
+///
+public class OracleQueries : IRelationalDatabaseInboxQueries
+{
+ ///
+ public string AddCommand { get; } =
+ "INSERT INTO {0} (CommandId,CommandType,CommandBody,Timestamp,ContextKey) " +
+ "VALUES (:CommandID,:CommandType,:CommandBody,:Timestamp,:ContextKey)";
+
+ ///
+ public string ExistsCommand { get; } =
+ "SELECT CommandId FROM {0} WHERE CommandId = :CommandID AND ContextKey = :ContextKey " +
+ "FETCH FIRST 1 ROW ONLY";
+
+ ///
+ public string GetCommand { get; } =
+ "SELECT * FROM {0} WHERE CommandId = :CommandID AND ContextKey = :ContextKey";
+}
diff --git a/src/Paramore.Brighter.Inbox.Oracle/Paramore.Brighter.Inbox.Oracle.csproj b/src/Paramore.Brighter.Inbox.Oracle/Paramore.Brighter.Inbox.Oracle.csproj
new file mode 100644
index 0000000000..456ba6d279
--- /dev/null
+++ b/src/Paramore.Brighter.Inbox.Oracle/Paramore.Brighter.Inbox.Oracle.csproj
@@ -0,0 +1,16 @@
+
+
+
+ Oracle Database inbox provider for Paramore.Brighter Command Processor. Enables inbox pattern implementation for idempotent command and event handling using Oracle as the persistence layer.
+ Rafael Andrade
+ $(BrighterMongoTargetFrameworks)
+ Oracle;Oracle Database;Database;Inbox;Idempotent;Command;Event;Command Processor;Relational Database;Brighter
+ enable
+
+
+
+
+
+
+
+
diff --git a/src/Paramore.Brighter.Locking.Oracle/OracleLockingProvider.cs b/src/Paramore.Brighter.Locking.Oracle/OracleLockingProvider.cs
new file mode 100644
index 0000000000..f8af289fd3
--- /dev/null
+++ b/src/Paramore.Brighter.Locking.Oracle/OracleLockingProvider.cs
@@ -0,0 +1,235 @@
+using System.Collections.Concurrent;
+using System.Data;
+using Microsoft.Extensions.Logging;
+using Oracle.ManagedDataAccess.Client;
+using Paramore.Brighter.Logging;
+using Paramore.Brighter.Oracle;
+
+namespace Paramore.Brighter.Locking.Oracle;
+
+///
+/// Provides Oracle-backed distributed locking using DBMS_LOCK APIs.
+///
+/// Provides Oracle connections used to allocate, request, and release locks.
+public partial class OracleLockingProvider(OracleConnectionProvider connectionProvider) : IDistributedLock, IAsyncDisposable
+{
+ private readonly ILogger _logger = ApplicationLogging.CreateLogger();
+ private readonly ConcurrentDictionary _connections = new();
+
+
+#if NETFRAMEWORK
+ ///
+ /// Releases all held Oracle connections tracked by this lock provider.
+ ///
+ /// A completed once all connections are released.
+ public ValueTask DisposeAsync()
+ {
+ foreach (var connection in _connections.Values)
+ {
+ connection.Close();
+ connection.Dispose();
+ }
+
+ _connections.Clear();
+ return new ValueTask();
+ }
+#else
+ ///
+ /// Releases all held Oracle connections tracked by this lock provider.
+ ///
+ /// A that completes when all connections are asynchronously released.
+ public async ValueTask DisposeAsync()
+ {
+ foreach (var connection in _connections.Values)
+ {
+ await connection.CloseAsync();
+ await connection.DisposeAsync();
+ }
+
+ _connections.Clear();
+ }
+#endif
+
+ ///
+ /// Attempts to obtain an exclusive lock for the specified resource.
+ ///
+ /// The resource identifier to lock.
+ /// A used to cancel the asynchronous operation.
+ ///
+ /// A containing the Oracle lock handle when the lock is acquired; otherwise .
+ ///
+ public async Task ObtainLockAsync(string resource, CancellationToken cancellationToken)
+ {
+ if (_connections.ContainsKey(resource))
+ {
+ Log.LockAlreadyHeld(_logger, resource);
+ return null;
+ }
+
+ string? lockHandler = null;
+
+ var connection = (OracleConnection)await connectionProvider.GetConnectionAsync(cancellationToken);
+ try
+ {
+ lockHandler = await AllocateLockHandleAsync(connection, resource);
+ if (string.IsNullOrEmpty(lockHandler))
+ {
+ Log.LockHandleAllocationFailed(_logger, resource);
+ return null;
+ }
+
+ var requestLock = await RequestLock(connection, lockHandler, 1);
+ if (requestLock > 0)
+ {
+ Log.LockNotGranted(_logger, resource, requestLock);
+ return null;
+ }
+
+ _connections.TryAdd(resource, connection);
+ Log.LockObtained(_logger, resource, lockHandler!);
+ return lockHandler;
+ }
+ catch (Exception e)
+ {
+ Log.LockAcquisitionFailed(_logger, e, resource);
+ if (!string.IsNullOrEmpty(lockHandler))
+ {
+ await ReleaseLockHandlerAsync(connection, lockHandler!);
+ }
+#if NETFRAMEWORK
+ connection.Close();
+ connection.Dispose();
+#else
+ await connection.CloseAsync();
+ await connection.DisposeAsync();
+#endif
+ return null;
+ }
+ }
+
+ ///
+ /// Releases a previously obtained lock for the specified resource.
+ ///
+ /// The resource identifier that is currently locked.
+ /// The lock identifier returned by .
+ /// A used to cancel the asynchronous operation.
+ /// A that completes when the lock release operation has finished.
+ public async Task ReleaseLockAsync(string resource, string lockId, CancellationToken cancellationToken)
+ {
+ if (!_connections.TryRemove(resource, out var connection))
+ {
+ Log.LockReleaseSkipped(_logger, resource, lockId);
+ return;
+ }
+
+ await ReleaseLockHandlerAsync(connection, lockId);
+ Log.LockReleased(_logger, resource, lockId);
+
+#if NETFRAMEWORK
+ connection.Close();
+ connection.Dispose();
+#else
+ await connection.CloseAsync();
+ await connection.DisposeAsync();
+#endif
+ }
+
+ private static partial class Log
+ {
+ [LoggerMessage(LogLevel.Information, "Unable to obtain lock for resource {Resource}; a connection is already tracking this lock")]
+ public static partial void LockAlreadyHeld(ILogger logger, string resource);
+
+ [LoggerMessage(LogLevel.Information, "Unable to obtain lock for resource {Resource}; Oracle DBMS_LOCK returned status {ResultCode}")]
+ public static partial void LockNotGranted(ILogger logger, string resource, int resultCode);
+
+ [LoggerMessage(LogLevel.Warning, "Unable to obtain lock for resource {Resource}; Oracle DBMS_LOCK did not return a lock handle")]
+ public static partial void LockHandleAllocationFailed(ILogger logger, string resource);
+
+ [LoggerMessage(LogLevel.Information, "Obtained lock for resource {Resource} with handle {LockHandle}")]
+ public static partial void LockObtained(ILogger logger, string resource, string lockHandle);
+
+ [LoggerMessage(LogLevel.Error, "Failed to obtain lock for resource {Resource}")]
+ public static partial void LockAcquisitionFailed(ILogger logger, Exception exception, string resource);
+
+ [LoggerMessage(LogLevel.Debug, "Skipping release for resource {Resource}; no tracked connection for lock {LockId}")]
+ public static partial void LockReleaseSkipped(ILogger logger, string resource, string lockId);
+
+ [LoggerMessage(LogLevel.Information, "Released lock {LockId} for resource {Resource}")]
+ public static partial void LockReleased(ILogger logger, string resource, string lockId);
+ }
+
+ private static async Task AllocateLockHandleAsync(OracleConnection conn, string lockName)
+ {
+ const string plsql = """
+ BEGIN
+ DBMS_LOCK.ALLOCATE_UNIQUE(:name, :handle);
+ END;
+ """;
+
+#if NETFRAMEWORK
+ using var cmd = new OracleCommand(plsql, conn);
+#else
+ await using var cmd = new OracleCommand(plsql, conn);
+#endif
+
+ var handle = new OracleParameter("handle", OracleDbType.Varchar2, 128)
+ {
+ Direction = ParameterDirection.Output
+ };
+
+ cmd.Parameters.Add(new OracleParameter("name", OracleDbType.Varchar2)
+ {
+ Value = lockName,
+ Direction = ParameterDirection.Input
+ });
+ cmd.Parameters.Add(handle);
+ await cmd.ExecuteNonQueryAsync();
+
+ return handle.Value?.ToString();
+ }
+
+
+ private static async Task RequestLock(OracleConnection conn, string? lockHandle, int timeoutSeconds)
+ {
+ // 6 = X_MODE (Exclusive Mode). release_on_commit = FALSE keeps lock alive across app updates.
+ const string plsql = """
+ BEGIN
+ :result := DBMS_LOCK.REQUEST(:lockhandle, 6, :timeout, FALSE);
+ END;
+ """;
+
+#if NETFRAMEWORK
+ using var cmd = new OracleCommand(plsql, conn);
+#else
+ await using var cmd = new OracleCommand(plsql, conn);
+#endif
+ var resultParam = new OracleParameter("result", OracleDbType.Int32, ParameterDirection.Output);
+
+ cmd.Parameters.Add(resultParam);
+ cmd.Parameters.Add("lockhandle", OracleDbType.Varchar2, lockHandle, ParameterDirection.Input);
+ cmd.Parameters.Add("timeout", OracleDbType.Int32, timeoutSeconds, ParameterDirection.Input);
+
+ await cmd.ExecuteNonQueryAsync();
+ return Convert.ToInt32(resultParam.Value?.ToString());
+ }
+
+ private static async Task ReleaseLockHandlerAsync(OracleConnection conn, string lockHandle)
+ {
+ const string plsql = """
+ BEGIN
+ :result := DBMS_LOCK.RELEASE(:lockhandle);
+ END;
+ """;
+
+#if NETFRAMEWORK
+ using var cmd = new OracleCommand(plsql, conn);
+#else
+ await using var cmd = new OracleCommand(plsql, conn);
+#endif
+ var resultParam = new OracleParameter("result", OracleDbType.Int32, ParameterDirection.Output);
+ cmd.Parameters.Add(resultParam);
+ cmd.Parameters.Add("lockhandle", OracleDbType.Varchar2, lockHandle, ParameterDirection.Input);
+
+ await cmd.ExecuteNonQueryAsync();
+ }
+}
diff --git a/src/Paramore.Brighter.Locking.Oracle/Paramore.Brighter.Locking.Oracle.csproj b/src/Paramore.Brighter.Locking.Oracle/Paramore.Brighter.Locking.Oracle.csproj
new file mode 100644
index 0000000000..029f33cb21
--- /dev/null
+++ b/src/Paramore.Brighter.Locking.Oracle/Paramore.Brighter.Locking.Oracle.csproj
@@ -0,0 +1,16 @@
+
+
+
+ Oracle Database distributed locking provider for Paramore.Brighter Command Processor. Provides lock coordination across multiple instances using Oracle DBMS_LOCK.
+ Rafael Andrade
+ $(BrighterMongoTargetFrameworks)
+ Oracle;Oracle Database;Database;Distributed Locking;Concurrency;Lock;Command Processor;Relational Database;Brighter
+ enable
+ enable
+
+
+
+
+
+
+
diff --git a/src/Paramore.Brighter.Oracle/OracleConnectionProvider.cs b/src/Paramore.Brighter.Oracle/OracleConnectionProvider.cs
new file mode 100644
index 0000000000..d5d6b72d20
--- /dev/null
+++ b/src/Paramore.Brighter.Oracle/OracleConnectionProvider.cs
@@ -0,0 +1,63 @@
+using System;
+using System.Data;
+using System.Data.Common;
+using System.Threading;
+using System.Threading.Tasks;
+using Oracle.ManagedDataAccess.Client;
+
+namespace Paramore.Brighter.Oracle;
+
+///
+/// A connection provider for Oracle using a connection string for database access.
+///
+public class OracleConnectionProvider : RelationalDbConnectionProvider
+{
+ private readonly string _connectionString;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The relational database configuration containing the connection string.
+ /// Thrown when the connection string is null or empty.
+ public OracleConnectionProvider(IAmARelationalDatabaseConfiguration configuration)
+ {
+ if (string.IsNullOrEmpty(configuration.ConnectionString))
+ {
+ throw new ArgumentNullException(nameof(configuration.ConnectionString));
+ }
+ _connectionString = configuration.ConnectionString;
+ }
+
+ ///
+ /// Creates and opens a new Oracle connection.
+ /// This is not a shared connection; you should manage its lifetime.
+ ///
+ /// An open .
+ public override DbConnection GetConnection()
+ {
+ var connection = new OracleConnection(_connectionString);
+ if (connection.State != ConnectionState.Open)
+ {
+ connection.Open();
+ }
+
+ return connection;
+ }
+
+ ///
+ /// Creates and opens a new Oracle connection asynchronously.
+ /// This is not a shared connection; you should manage its lifetime.
+ ///
+ /// A cancellation token to cancel the operation.
+ /// An open .
+ public override async Task GetConnectionAsync(CancellationToken cancellationToken = default)
+ {
+ var connection = new OracleConnection(_connectionString);
+ if (connection.State != ConnectionState.Open)
+ {
+ await connection.OpenAsync(cancellationToken);
+ }
+
+ return connection;
+ }
+}
diff --git a/src/Paramore.Brighter.Oracle/OracleTransactionProvider.cs b/src/Paramore.Brighter.Oracle/OracleTransactionProvider.cs
new file mode 100644
index 0000000000..301d925ac2
--- /dev/null
+++ b/src/Paramore.Brighter.Oracle/OracleTransactionProvider.cs
@@ -0,0 +1,142 @@
+// The MIT License (MIT)
+// Copyright © 2014 Ian Cooper
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+using System.Data;
+using System.Data.Common;
+using System.Threading;
+using System.Threading.Tasks;
+using Oracle.ManagedDataAccess.Client;
+
+namespace Paramore.Brighter.Oracle;
+
+///
+/// A connection and transaction provider for Oracle, intended for use with a Unit of Work.
+/// The connection and transaction are shared across operations within the same unit of work.
+///
+public class OracleTransactionProvider : RelationalDbTransactionProvider
+{
+ private readonly string _connectionString;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The relational database configuration containing the connection string.
+ /// Thrown when the connection string is null or empty.
+ public OracleTransactionProvider(IAmARelationalDatabaseConfiguration configuration)
+ {
+ if (string.IsNullOrEmpty(configuration.ConnectionString))
+ {
+ throw new ArgumentNullException(nameof(configuration.ConnectionString));
+ }
+
+ _connectionString = configuration.ConnectionString;
+ }
+
+#if !NETFRAMEWORK
+ ///
+ /// Commits the current transaction asynchronously and releases it.
+ ///
+ /// A cancellation token to cancel the operation.
+ public override async Task CommitAsync(CancellationToken cancellationToken)
+ {
+ if (HasOpenTransaction)
+ {
+ var transaction = (OracleTransaction)Transaction!;
+ await transaction.CommitAsync(cancellationToken);
+
+ Transaction = null;
+ }
+ }
+#endif
+
+ ///
+ /// Creates and opens a new Oracle connection.
+ /// This is not a shared connection; you should manage its lifetime.
+ ///
+ /// An open .
+ public override DbConnection GetConnection()
+ {
+ Connection ??= new OracleConnection(_connectionString);
+ if (Connection.State != ConnectionState.Open)
+ {
+ Connection.Open();
+ }
+
+ return Connection;
+ }
+
+ ///
+ /// Creates and opens a new Oracle connection asynchronously.
+ /// This is not a shared connection; you should manage its lifetime.
+ ///
+ /// A cancellation token to cancel the operation.
+ /// An open .
+ public override async Task GetConnectionAsync(CancellationToken cancellationToken = default)
+ {
+ Connection ??= new OracleConnection(_connectionString);
+ if (Connection.State != ConnectionState.Open)
+ {
+ await Connection.OpenAsync(cancellationToken);
+ }
+
+ return Connection;
+ }
+
+ ///
+ /// Creates and opens a shared Oracle transaction.
+ /// Reuses the existing transaction if one is already open.
+ ///
+ /// The shared .
+ public override DbTransaction GetTransaction()
+ {
+ Connection ??= GetConnection();
+
+ if (!HasOpenTransaction)
+ {
+ Transaction = ((OracleConnection)Connection).BeginTransaction();
+ }
+
+ return Transaction!;
+ }
+
+ ///
+ /// Creates and opens a shared Oracle transaction asynchronously.
+ /// Reuses the existing transaction if one is already open.
+ ///
+ /// A cancellation token to cancel the operation.
+ /// The shared .
+ public override async Task GetTransactionAsync(CancellationToken cancellationToken = default)
+ {
+ Connection ??= await GetConnectionAsync(cancellationToken);
+
+ if (!HasOpenTransaction)
+ {
+#if NETFRAMEWORK
+ Transaction = ((OracleConnection)Connection).BeginTransaction();
+#else
+ Transaction = await ((OracleConnection)Connection).BeginTransactionAsync(cancellationToken);
+#endif
+ }
+
+ return Transaction!;
+ }
+}
diff --git a/src/Paramore.Brighter.Oracle/Paramore.Brighter.Oracle.csproj b/src/Paramore.Brighter.Oracle/Paramore.Brighter.Oracle.csproj
new file mode 100644
index 0000000000..39c1dad730
--- /dev/null
+++ b/src/Paramore.Brighter.Oracle/Paramore.Brighter.Oracle.csproj
@@ -0,0 +1,22 @@
+
+
+
+ Oracle Database provider for Paramore.Brighter Command Processor. Provides common components for connecting to Oracle, including connection and transaction management for inbox, outbox, and messaging gateway implementations.
+ Rafael Andrade
+ $(BrighterMongoTargetFrameworks)
+ Oracle;Oracle Database;Database;Connection Provider;Transaction;Command;Event;Command Processor;Brighter
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Paramore.Brighter.Outbox.Oracle/OracleOutbox.cs b/src/Paramore.Brighter.Outbox.Oracle/OracleOutbox.cs
new file mode 100644
index 0000000000..929fb10d7c
--- /dev/null
+++ b/src/Paramore.Brighter.Outbox.Oracle/OracleOutbox.cs
@@ -0,0 +1,147 @@
+// The MIT License (MIT)
+// Copyright © 2014 Ian Cooper
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+using System.Collections.Generic;
+using System.Data;
+using System.Data.Common;
+using System.Linq;
+using Oracle.ManagedDataAccess.Client;
+using Paramore.Brighter.Logging;
+using Paramore.Brighter.Observability;
+using Paramore.Brighter.Oracle;
+
+namespace Paramore.Brighter.Outbox.Oracle;
+
+///
+/// Implements an Outbox using Oracle as a backing store.
+/// Requires Oracle 12c or later.
+///
+public class OracleOutbox : RelationDatabaseOutbox
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The relational database configuration.
+ /// The connection provider.
+ public OracleOutbox(IAmARelationalDatabaseConfiguration configuration,
+ IAmARelationalDbConnectionProvider connectionProvider)
+ : base(DbSystem.Oracle, configuration, connectionProvider, new OracleQueries(),
+ ApplicationLogging.CreateLogger())
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class using a default connection provider.
+ ///
+ /// The relational database configuration.
+ public OracleOutbox(IAmARelationalDatabaseConfiguration configuration)
+ : this(configuration, new OracleConnectionProvider(configuration))
+ {
+ }
+
+ // ORA-00001: unique constraint violated
+ private const int DuplicateValue = 1;
+
+ ///
+ protected override bool IsExceptionUniqueOrDuplicateIssue(Exception ex)
+ {
+ return ex is OracleException { Number: DuplicateValue };
+ }
+
+ ///
+ protected override IDbDataParameter CreateSqlParameter(string parameterName, object? value)
+ {
+ parameterName = parameterName.Replace("@", ":");
+ return new OracleParameter { ParameterName = parameterName, Value = value ?? DBNull.Value };
+ }
+
+ ///
+ protected override IDbDataParameter CreateSqlParameter(string parameterName, DbType dbType, object? value)
+ {
+ parameterName = parameterName.Replace("@", ":");
+ return new OracleParameter { ParameterName = parameterName, DbType = dbType, Value = value ?? DBNull.Value };
+ }
+
+ ///
+ /// Sets to so that
+ /// named :param placeholders in the SQL are matched to parameters by name.
+ protected override DbCommand CreateCommand(DbConnection connection, string sqlText, int outBoxTimeout,
+ params IDbDataParameter[] parameters)
+ {
+ var oracleConnection = (OracleConnection)connection;
+ var command = oracleConnection.CreateCommand();
+ command.BindByName = true;
+ command.CommandTimeout = outBoxTimeout < 0 ? 0 : outBoxTimeout;
+ command.CommandText = sqlText;
+ command.Parameters.AddRange(parameters);
+ return command;
+ }
+
+ ///
+ ///
+ /// Generates SELECT … FROM DUAL UNION ALL SELECT … FROM DUAL rather than
+ /// multi-row VALUES tuples, which Oracle does not support before version 23c.
+ ///
+ protected override (string insertClause, IDbDataParameter[] parameters) GenerateBulkInsert(List messages)
+ {
+ var selects = new List();
+ var parameters = new List();
+
+ for (var i = 0; i < messages.Count; i++)
+ {
+ selects.Add(
+ $"SELECT :p{i}_MessageId,:p{i}_MessageType,:p{i}_Topic,:p{i}_Timestamp,:p{i}_CorrelationId," +
+ $":p{i}_ReplyTo,:p{i}_ContentType,:p{i}_PartitionKey,:p{i}_HeaderBag,:p{i}_Body," +
+ $":p{i}_Source,:p{i}_Type,:p{i}_DataSchema,:p{i}_Subject,:p{i}_TraceParent,:p{i}_TraceState," +
+ $":p{i}_Baggage,:p{i}_WorkflowId,:p{i}_JobId FROM DUAL");
+ parameters.AddRange(InitAddDbParameters(messages[i], i));
+ }
+
+ return (string.Join(" UNION ALL ", selects), parameters.ToArray());
+ }
+
+ protected override (string inClause, IDbDataParameter[] parameters) GenerateInClauseAndAddParameters(
+ List messageIds)
+ {
+ var paramNames = messageIds.Select((_, i) => ":p" + i).ToArray();
+
+ var parameters = new IDbDataParameter[messageIds.Count];
+ for (int i = 0; i < paramNames.Length; i++)
+ {
+ parameters[i] = CreateSqlParameter(paramNames[i], DbType.String, messageIds[i]);
+ }
+
+ return (string.Join(",", paramNames), parameters);
+ }
+
+ ///
+ protected override DateTimeOffset GetTimeStamp(DbDataReader dr)
+ {
+ if (!TryGetOrdinal(dr, TimestampColumnName, out var ordinal) || dr.IsDBNull(ordinal))
+ {
+ return DateTimeOffset.MinValue;
+ }
+
+ var reader = (OracleDataReader)dr;
+ return reader.GetDateTimeOffset(ordinal);
+ }
+}
diff --git a/src/Paramore.Brighter.Outbox.Oracle/OracleOutboxBuilder.cs b/src/Paramore.Brighter.Outbox.Oracle/OracleOutboxBuilder.cs
new file mode 100644
index 0000000000..0c4e7de36c
--- /dev/null
+++ b/src/Paramore.Brighter.Outbox.Oracle/OracleOutboxBuilder.cs
@@ -0,0 +1,130 @@
+// The MIT License (MIT)
+// Copyright © 2014 Ian Cooper
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System;
+
+namespace Paramore.Brighter.Outbox.Oracle;
+
+///
+/// Provides DDL statements for creating and checking the existence of an Outbox table in Oracle.
+/// Requires Oracle 12c or later (uses identity columns and TIMESTAMP WITH TIME ZONE).
+///
+public class OracleOutboxBuilder
+{
+ private const string TextOutboxDdl =
+ """
+ CREATE TABLE {0}
+ (
+ Id NUMBER(19) GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
+ MessageId VARCHAR2(255 CHAR) UNIQUE NOT NULL,
+ Topic VARCHAR2(255 CHAR),
+ MessageType VARCHAR2(32 CHAR),
+ Timestamp TIMESTAMP WITH TIME ZONE,
+ CorrelationId VARCHAR2(255 CHAR),
+ ReplyTo VARCHAR2(255 CHAR),
+ ContentType VARCHAR2(128 CHAR),
+ PartitionKey VARCHAR2(128 CHAR),
+ WorkflowId VARCHAR2(255 CHAR),
+ JobId VARCHAR2(255 CHAR),
+ Dispatched TIMESTAMP WITH TIME ZONE,
+ HeaderBag CLOB,
+ Body CLOB,
+ Source VARCHAR2(255 CHAR),
+ Type VARCHAR2(255 CHAR),
+ DataSchema VARCHAR2(255 CHAR),
+ Subject VARCHAR2(255 CHAR),
+ TraceParent VARCHAR2(255 CHAR),
+ TraceState VARCHAR2(255 CHAR),
+ Baggage CLOB,
+ DataRef VARCHAR2(255 CHAR),
+ SpecVersion VARCHAR2(255 CHAR)
+ );
+ """;
+
+ private const string BinaryOutboxDdl =
+ """
+ CREATE TABLE {0}
+ (
+ Id NUMBER(19) GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
+ MessageId VARCHAR2(255 CHAR) UNIQUE NOT NULL,
+ Topic VARCHAR2(255 CHAR),
+ MessageType VARCHAR2(32 CHAR),
+ Timestamp TIMESTAMP WITH TIME ZONE,
+ CorrelationId VARCHAR2(255 CHAR),
+ ReplyTo VARCHAR2(255 CHAR),
+ ContentType VARCHAR2(128 CHAR),
+ PartitionKey VARCHAR2(128 CHAR),
+ WorkflowId VARCHAR2(255 CHAR),
+ JobId VARCHAR2(255 CHAR),
+ Dispatched TIMESTAMP WITH TIME ZONE,
+ HeaderBag CLOB,
+ Body BLOB,
+ Source VARCHAR2(255 CHAR),
+ Type VARCHAR2(255 CHAR),
+ DataSchema VARCHAR2(255 CHAR),
+ Subject VARCHAR2(255 CHAR),
+ TraceParent VARCHAR2(255 CHAR),
+ TraceState VARCHAR2(255 CHAR),
+ Baggage CLOB,
+ DataRef VARCHAR2(255 CHAR),
+ SpecVersion VARCHAR2(255 CHAR)
+ );
+ """;
+
+ private const string OutboxExistsSql =
+ """
+ SELECT COUNT(1)
+ FROM ALL_TABLES
+ WHERE OWNER = '{0}'
+ AND TABLE_NAME = '{1}';
+ """;
+
+ ///
+ /// Gets the SQL statement that checks whether the Outbox table already exists in the given schema.
+ ///
+ /// The table name to check.
+ /// The Oracle schema (owner) to search within.
+ /// A SQL query that returns 1 if the table exists, 0 otherwise.
+ public static string GetExistsQuery(string outboxTable, string ownerName)
+ {
+ return string.Format(OutboxExistsSql, outboxTable, ownerName);
+ }
+
+ ///
+ /// Gets the DDL statement required to create the Outbox table in Oracle.
+ ///
+ /// The name to give the Outbox table.
+ ///
+ /// When , the Body column is created as BLOB;
+ /// otherwise it is created as CLOB.
+ ///
+ /// A CREATE TABLE statement formatted with the given table name.
+ /// Thrown when is null or empty.
+ public static string GetDDL(string outboxTableName, bool hasBinaryMessagePayload = false)
+ {
+ if (string.IsNullOrEmpty(outboxTableName))
+ {
+ throw new ArgumentNullException(outboxTableName, $"You must provide a tablename for the OutBox table");
+ }
+
+ return string.Format(hasBinaryMessagePayload ? BinaryOutboxDdl : TextOutboxDdl, outboxTableName);
+ }
+}
diff --git a/src/Paramore.Brighter.Outbox.Oracle/OracleQueries.cs b/src/Paramore.Brighter.Outbox.Oracle/OracleQueries.cs
new file mode 100644
index 0000000000..0fc2541e6a
--- /dev/null
+++ b/src/Paramore.Brighter.Outbox.Oracle/OracleQueries.cs
@@ -0,0 +1,87 @@
+// The MIT License (MIT)
+// Copyright © 2014 Ian Cooper
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+namespace Paramore.Brighter.Outbox.Oracle;
+
+///
+/// Oracle-specific SQL queries for the Brighter Outbox.
+/// Uses OFFSET … ROWS FETCH NEXT … ROWS ONLY for pagination (Oracle 12c+)
+/// and named bind variables prefixed with :.
+/// Parameter names are matched by name via .
+///
+public class OracleQueries : IRelationDatabaseOutboxQueries
+{
+ ///
+ public string PagedDispatchedCommand { get; } =
+ "SELECT * FROM {0} WHERE Dispatched IS NOT NULL AND Dispatched < :DispatchedSince " +
+ "ORDER BY Timestamp DESC OFFSET :Skip ROWS FETCH NEXT :Take ROWS ONLY";
+
+ ///
+ public string PagedReadCommand { get; } =
+ "SELECT * FROM {0} ORDER BY Timestamp ASC OFFSET :Skip ROWS FETCH NEXT :Take ROWS ONLY";
+
+ ///
+ public string PagedOutstandingCommand { get; } =
+ "SELECT * FROM {0} WHERE Dispatched IS NULL AND Timestamp < :TimestampSince {1} " +
+ "ORDER BY Timestamp DESC OFFSET :Skip ROWS FETCH NEXT :Take ROWS ONLY";
+
+ ///
+ public string PagedOutstandingCommandInStatement { get; } = "AND Topic NOT IN ( {0} )";
+
+ ///
+ public string AddCommand { get; } =
+ "INSERT INTO {0} (MessageId,MessageType,Topic,Timestamp,CorrelationId,ReplyTo,ContentType,PartitionKey," +
+ "HeaderBag,Body,Source,Type,DataSchema,Subject,TraceParent,TraceState,Baggage,WorkflowId,JobId) " +
+ "VALUES (:MessageId,:MessageType,:Topic,:Timestamp,:CorrelationId,:ReplyTo,:ContentType,:PartitionKey," +
+ ":HeaderBag,:Body,:Source,:Type,:DataSchema,:Subject,:TraceParent,:TraceState,:Baggage,:WorkflowId,:JobId)";
+
+ ///
+ ///
+ /// {1} is replaced with SELECT :p0_… FROM DUAL UNION ALL SELECT :p1_… FROM DUAL,
+ /// generated by . Oracle does not support
+ /// multi-row VALUES clauses before version 23c.
+ ///
+ public string BulkAddCommand { get; } =
+ "INSERT INTO {0} (MessageId,MessageType,Topic,Timestamp,CorrelationId,ReplyTo,ContentType,PartitionKey," +
+ "HeaderBag,Body,Source,Type,DataSchema,Subject,TraceParent,TraceState,Baggage,WorkflowId,JobId) {1}";
+
+ ///
+ public string MarkDispatchedCommand { get; } =
+ "UPDATE {0} SET Dispatched = :DispatchedAt WHERE MessageId = :MessageId";
+
+ ///
+ public string MarkMultipleDispatchedCommand { get; } =
+ "UPDATE {0} SET Dispatched = :DispatchedAt WHERE MessageId IN ( {1} )";
+
+ ///
+ public string GetMessageCommand { get; } = "SELECT * FROM {0} WHERE MessageId = :MessageId";
+
+ ///
+ public string GetMessagesCommand { get; } =
+ "SELECT * FROM {0} WHERE MessageId IN ( {1} ) ORDER BY Timestamp ASC";
+
+ ///
+ public string DeleteMessagesCommand { get; } = "DELETE FROM {0} WHERE MessageId IN ( {1} )";
+
+ ///
+ public string GetNumberOfOutstandingMessagesCommand { get; } =
+ "SELECT COUNT(1) FROM {0} WHERE Dispatched IS NULL";
+}
diff --git a/src/Paramore.Brighter.Outbox.Oracle/Paramore.Brighter.Outbox.Oracle.csproj b/src/Paramore.Brighter.Outbox.Oracle/Paramore.Brighter.Outbox.Oracle.csproj
new file mode 100644
index 0000000000..42564c3ebd
--- /dev/null
+++ b/src/Paramore.Brighter.Outbox.Oracle/Paramore.Brighter.Outbox.Oracle.csproj
@@ -0,0 +1,15 @@
+
+
+
+ Oracle Database outbox implementation for Paramore.Brighter Command Processor. Provides reliable message publishing with outbox pattern using Oracle for transactional consistency and guaranteed delivery.
+ Rafael Andrade
+ $(BrighterMongoTargetFrameworks)
+ Oracle;Oracle Database;Database;Outbox;Transactional;Command;Event;Service Activator;Decoupled;Invocation;Messaging;Remote;Command Dispatcher;Command Processor;Request;Service;Task Queue;Work Queue;Retry;Circuit Breaker;Availability;Brighter
+ enable
+
+
+
+
+
+
+
diff --git a/tests/Paramore.Brighter.Oracle.Tests/BoxProvisioning/ExpectedMigrationVersions.cs b/tests/Paramore.Brighter.Oracle.Tests/BoxProvisioning/ExpectedMigrationVersions.cs
new file mode 100644
index 0000000000..73a880c004
--- /dev/null
+++ b/tests/Paramore.Brighter.Oracle.Tests/BoxProvisioning/ExpectedMigrationVersions.cs
@@ -0,0 +1,7 @@
+namespace Paramore.Brighter.Oracle.Tests.BoxProvisioning;
+
+internal static class ExpectedMigrationVersions
+{
+ public const int OutboxLatest = 1;
+ public const int InboxLatest = 1;
+}
diff --git a/tests/Paramore.Brighter.Oracle.Tests/BoxProvisioning/OracleBoxProvisioningTestHelper.cs b/tests/Paramore.Brighter.Oracle.Tests/BoxProvisioning/OracleBoxProvisioningTestHelper.cs
new file mode 100644
index 0000000000..5f3f758ae6
--- /dev/null
+++ b/tests/Paramore.Brighter.Oracle.Tests/BoxProvisioning/OracleBoxProvisioningTestHelper.cs
@@ -0,0 +1,67 @@
+using System;
+using System.Threading.Tasks;
+using Oracle.ManagedDataAccess.Client;
+
+namespace Paramore.Brighter.Oracle.Tests.BoxProvisioning;
+
+internal static class OracleBoxProvisioningTestHelper
+{
+ public static async Task TableExistsAsync(string connectionString, string tableName)
+ {
+ await using var connection = new OracleConnection(connectionString);
+ await connection.OpenAsync();
+
+ await using var command = connection.CreateCommand();
+ command.BindByName = true;
+ command.CommandText = """
+ SELECT COUNT(1) FROM ALL_TABLES
+ WHERE OWNER = SYS_CONTEXT('USERENV', 'CURRENT_SCHEMA')
+ AND TABLE_NAME = UPPER(:TableName)
+ """;
+ command.Parameters.Add(new OracleParameter("TableName", tableName));
+
+ var count = Convert.ToInt32(await command.ExecuteScalarAsync());
+ return count > 0;
+ }
+
+ public static async Task HistoryCountForVersionAsync(string connectionString, string tableName, int version)
+ {
+ await using var connection = new OracleConnection(connectionString);
+ await connection.OpenAsync();
+
+ await using var command = connection.CreateCommand();
+ command.BindByName = true;
+ command.CommandText = """
+ SELECT COUNT(1) FROM BRIGHTER_MIGRATION_HISTORY
+ WHERE BoxTableName = :BoxTableName
+ AND SchemaName = SYS_CONTEXT('USERENV', 'CURRENT_SCHEMA')
+ AND MigrationVersion = :MigrationVersion
+ """;
+ command.Parameters.Add(new OracleParameter("BoxTableName", tableName));
+ command.Parameters.Add(new OracleParameter("MigrationVersion", version));
+
+ return Convert.ToInt32(await command.ExecuteScalarAsync());
+ }
+
+ public static async Task DropTableIfExistsAsync(string connectionString, string tableName)
+ {
+ await using var connection = new OracleConnection(connectionString);
+ await connection.OpenAsync();
+
+ await using var command = connection.CreateCommand();
+ command.BindByName = true;
+ command.CommandText = """
+ BEGIN
+ EXECUTE IMMEDIATE 'DROP TABLE ' || :TableName;
+ EXCEPTION
+ WHEN OTHERS THEN
+ IF SQLCODE != -942 THEN
+ RAISE;
+ END IF;
+ END;
+ """;
+ command.Parameters.Add(new OracleParameter("TableName", tableName));
+
+ await command.ExecuteNonQueryAsync();
+ }
+}
diff --git a/tests/Paramore.Brighter.Oracle.Tests/BoxProvisioning/When_oracle_inbox_provisioner_runs_on_fresh_database_it_should_create_inbox_table.cs b/tests/Paramore.Brighter.Oracle.Tests/BoxProvisioning/When_oracle_inbox_provisioner_runs_on_fresh_database_it_should_create_inbox_table.cs
new file mode 100644
index 0000000000..c1f2e1fcbc
--- /dev/null
+++ b/tests/Paramore.Brighter.Oracle.Tests/BoxProvisioning/When_oracle_inbox_provisioner_runs_on_fresh_database_it_should_create_inbox_table.cs
@@ -0,0 +1,57 @@
+using System;
+using System.Threading.Tasks;
+using Paramore.Brighter.BoxProvisioning.Oracle;
+using Xunit;
+
+namespace Paramore.Brighter.Oracle.Tests.BoxProvisioning;
+
+public class OracleInboxProvisionerFreshDatabaseTests : IAsyncLifetime
+{
+ private readonly string _connectionString = Const.DefaultConnectingString;
+ private readonly string _tableName;
+ private readonly OracleInboxProvisioner _provisioner;
+
+ public OracleInboxProvisionerFreshDatabaseTests()
+ {
+ _tableName = $"TEST_INBOX_{Guid.NewGuid():N}";
+
+ var configuration = new RelationalDatabaseConfiguration(
+ _connectionString,
+ inboxTableName: _tableName);
+
+ var runner = new OracleBoxMigrationRunner(
+ new OracleInboxMigrationCatalog(),
+ configuration,
+ TimeSpan.FromSeconds(30));
+
+ _provisioner = new OracleInboxProvisioner(
+ new OracleBoxDetectionHelper(),
+ new OracleInboxMigrationCatalog(),
+ new OraclePayloadModeValidator(),
+ configuration,
+ runner);
+ }
+
+ [Fact]
+ public async Task When_inbox_provisioner_runs_on_fresh_database_it_should_create_inbox_table()
+ {
+ await _provisioner.ProvisionAsync();
+
+ var tableExists = await OracleBoxProvisioningTestHelper.TableExistsAsync(_connectionString, _tableName);
+ Assert.True(tableExists);
+
+ var historyCount = await OracleBoxProvisioningTestHelper.HistoryCountForVersionAsync(
+ _connectionString,
+ _tableName,
+ ExpectedMigrationVersions.InboxLatest);
+
+ Assert.Equal(1, historyCount);
+ }
+
+ public Task InitializeAsync() => Task.CompletedTask;
+
+ public async Task DisposeAsync()
+ {
+ await OracleBoxProvisioningTestHelper.DropTableIfExistsAsync(_connectionString, _tableName);
+ }
+}
diff --git a/tests/Paramore.Brighter.Oracle.Tests/BoxProvisioning/When_oracle_outbox_provisioner_runs_on_fresh_database_it_should_create_outbox_table.cs b/tests/Paramore.Brighter.Oracle.Tests/BoxProvisioning/When_oracle_outbox_provisioner_runs_on_fresh_database_it_should_create_outbox_table.cs
new file mode 100644
index 0000000000..e70cf83cca
--- /dev/null
+++ b/tests/Paramore.Brighter.Oracle.Tests/BoxProvisioning/When_oracle_outbox_provisioner_runs_on_fresh_database_it_should_create_outbox_table.cs
@@ -0,0 +1,57 @@
+using System;
+using System.Threading.Tasks;
+using Paramore.Brighter.BoxProvisioning.Oracle;
+using Xunit;
+
+namespace Paramore.Brighter.Oracle.Tests.BoxProvisioning;
+
+public class OracleOutboxProvisionerFreshDatabaseTests : IAsyncLifetime
+{
+ private readonly string _connectionString = Const.DefaultConnectingString;
+ private readonly string _tableName;
+ private readonly OracleOutboxProvisioner _provisioner;
+
+ public OracleOutboxProvisionerFreshDatabaseTests()
+ {
+ _tableName = $"TEST_OUTBOX_{Guid.NewGuid():N}";
+
+ var configuration = new RelationalDatabaseConfiguration(
+ _connectionString,
+ outBoxTableName: _tableName);
+
+ var runner = new OracleBoxMigrationRunner(
+ new OracleOutboxMigrationCatalog(),
+ configuration,
+ TimeSpan.FromSeconds(30));
+
+ _provisioner = new OracleOutboxProvisioner(
+ new OracleBoxDetectionHelper(),
+ new OracleOutboxMigrationCatalog(),
+ new OraclePayloadModeValidator(),
+ configuration,
+ runner);
+ }
+
+ [Fact]
+ public async Task When_outbox_provisioner_runs_on_fresh_database_it_should_create_outbox_table()
+ {
+ await _provisioner.ProvisionAsync();
+
+ var tableExists = await OracleBoxProvisioningTestHelper.TableExistsAsync(_connectionString, _tableName);
+ Assert.True(tableExists);
+
+ var historyCount = await OracleBoxProvisioningTestHelper.HistoryCountForVersionAsync(
+ _connectionString,
+ _tableName,
+ ExpectedMigrationVersions.OutboxLatest);
+
+ Assert.Equal(1, historyCount);
+ }
+
+ public Task InitializeAsync() => Task.CompletedTask;
+
+ public async Task DisposeAsync()
+ {
+ await OracleBoxProvisioningTestHelper.DropTableIfExistsAsync(_connectionString, _tableName);
+ }
+}
diff --git a/tests/Paramore.Brighter.Oracle.Tests/Const.cs b/tests/Paramore.Brighter.Oracle.Tests/Const.cs
new file mode 100644
index 0000000000..a6794fd4c8
--- /dev/null
+++ b/tests/Paramore.Brighter.Oracle.Tests/Const.cs
@@ -0,0 +1,9 @@
+namespace Paramore.Brighter.Oracle.Tests;
+
+public static class Const
+{
+ public const string DefaultConnectingString =
+ "User Id=brighter;Password=BrighterTests1!;Data Source=//localhost:1521/FREEPDB1";
+
+ public const string TablePrefix = "Table";
+}
diff --git a/tests/Paramore.Brighter.Oracle.Tests/DefaultMessageAssertion.cs b/tests/Paramore.Brighter.Oracle.Tests/DefaultMessageAssertion.cs
new file mode 100644
index 0000000000..80fb3e469d
--- /dev/null
+++ b/tests/Paramore.Brighter.Oracle.Tests/DefaultMessageAssertion.cs
@@ -0,0 +1,64 @@
+#region Licence
+
+/* The MIT License (MIT)
+Copyright © 2014 Ian Cooper
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE. */
+
+#endregion
+
+//
+// This file is auto-generated by Paramore.Brighter.Test.Generator
+//
+
+namespace Paramore.Brighter.Oracle.Tests;
+
+///
+/// Default implementation of that asserts equality of message headers and body.
+///
+public class DefaultMessageAssertion : IAmAMessageAssertion
+{
+ ///
+ /// Asserts that the actual message matches the expected message by comparing all header properties and body value.
+ ///
+ /// The expected message.
+ /// The actual message to compare.
+ public void Assert(Message expected, Message actual)
+ {
+ Xunit.Assert.Equal(expected.Header.MessageType, actual.Header.MessageType);
+ Xunit.Assert.Equal(expected.Header.ContentType, actual.Header.ContentType);
+ Xunit.Assert.Equal(expected.Header.CorrelationId, actual.Header.CorrelationId);
+ Xunit.Assert.Equal(expected.Header.DataSchema, actual.Header.DataSchema);
+ Xunit.Assert.Equal(expected.Header.MessageId, actual.Header.MessageId);
+ Xunit.Assert.Equal(expected.Header.PartitionKey, actual.Header.PartitionKey);
+ Xunit.Assert.Equal(expected.Header.ReplyTo, actual.Header.ReplyTo);
+ Xunit.Assert.Equal(expected.Header.Subject, actual.Header.Subject);
+ Xunit.Assert.Equal(expected.Header.SpecVersion, actual.Header.SpecVersion);
+ Xunit.Assert.Equal(expected.Header.Source, actual.Header.Source);
+ Xunit.Assert.Equal(expected.Header.Topic, actual.Header.Topic);
+ Xunit.Assert.Equal(expected.Header.TimeStamp.ToString("yyyy-MM-ddTHH:mm:ss"), actual.Header.TimeStamp.ToString("yyyy-MM-ddTHH:mm:ss"));
+ Xunit.Assert.Equal(expected.Header.Type, actual.Header.Type);
+ Xunit.Assert.Equal(expected.Header.HandledCount, actual.Header.HandledCount);
+ Xunit.Assert.Equal(System.TimeSpan.Zero, actual.Header.Delayed);
+ Xunit.Assert.Equal(expected.Body.Value, actual.Body.Value);
+ Xunit.Assert.Equal(expected.Header.TraceParent, actual.Header.TraceParent);
+ Xunit.Assert.Equal(expected.Header.TraceState, actual.Header.TraceState);
+ Xunit.Assert.Equal(expected.Header.Baggage, actual.Header.Baggage);
+ }
+}
diff --git a/tests/Paramore.Brighter.Oracle.Tests/DefaultMessageBuilder.cs b/tests/Paramore.Brighter.Oracle.Tests/DefaultMessageBuilder.cs
new file mode 100644
index 0000000000..c8da9c2196
--- /dev/null
+++ b/tests/Paramore.Brighter.Oracle.Tests/DefaultMessageBuilder.cs
@@ -0,0 +1,256 @@
+#region Licence
+
+/* The MIT License (MIT)
+Copyright © 2014 Ian Cooper
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE. */
+
+#endregion
+
+//
+// This file is auto-generated by Paramore.Brighter.Test.Generator
+//
+using System;
+using System.Collections.Generic;
+using System.Net.Mime;
+using System.Text;
+
+using Paramore.Brighter.Observability;
+
+namespace Paramore.Brighter.Oracle.Tests;
+
+///
+/// Default implementation of that creates messages with randomly generated test data.
+///
+public class DefaultMessageBuilder : IAmAMessageBuilder
+{
+ private Dictionary _bag = new()
+ {
+ ["header1"] = Uuid.NewAsString(),
+ ["header2"] = Uuid.NewAsString(),
+ ["header3"] = Uuid.NewAsString(),
+ ["header4"] = Uuid.NewAsString(),
+ ["header5"] = Uuid.NewAsString()
+ };
+ private Baggage _baggage = new();
+ private ContentType _contentType = new ContentType(MediaTypeNames.Text.Plain);
+ private Id _correlationId = Id.Random();
+ private Uri? _dataSchema = new Uri($"https://{Uuid.New():N}.test");
+ private string? _dataRef = Uuid.NewAsString();
+ private TimeSpan? _delayed;
+ private Id? _jobId = Id.Random();
+ private Id? _messageId = Id.Random();
+ private MessageType _messageType = MessageType.MT_EVENT;
+ private PartitionKey _partitionKey = PartitionKey.Empty;
+ private RoutingKey? _replyTo = new RoutingKey(Uuid.NewAsString());
+ private string? _subject = Uuid.NewAsString();
+ private string? _specVersion = "1.0";
+ private Uri? _source = new Uri(Uuid.NewAsString(), UriKind.Relative);
+ private RoutingKey _topic = new RoutingKey(Uuid.NewAsString());
+ private DateTimeOffset _timeStamp = DateTimeOffset.UtcNow;
+ private TraceParent? _traceParent = new TraceParent(Uuid.NewAsString());
+ private TraceState? _traceState = new TraceState(Uuid.NewAsString());
+ private CloudEventsType _type = new CloudEventsType(Uuid.NewAsString());
+ private Id? _workflowId = Id.Random();
+ private byte[] _body = Encoding.UTF8.GetBytes(Uuid.NewAsString());
+
+ ///
+ public IAmAMessageBuilder SetBag(Dictionary bag)
+ {
+ _bag = bag;
+ return this;
+ }
+
+ ///
+ public IAmAMessageBuilder SetBaggage(Baggage baggage)
+ {
+ _baggage = baggage;
+ return this;
+ }
+
+ ///
+ public IAmAMessageBuilder SetContentType(ContentType contentType)
+ {
+ _contentType = contentType;
+ return this;
+ }
+
+ ///
+ public IAmAMessageBuilder SetCorrelationId(Id correlationId)
+ {
+ _correlationId = correlationId;
+ return this;
+ }
+
+ ///
+ public IAmAMessageBuilder SetDataSchema(Uri? dataSchema)
+ {
+ _dataSchema = dataSchema;
+ return this;
+ }
+
+ ///
+ public IAmAMessageBuilder SetDataRef(string? dataRef)
+ {
+ _dataRef = dataRef;
+ return this;
+ }
+
+ ///
+ public IAmAMessageBuilder SetDelayed(TimeSpan? delayed)
+ {
+ _delayed = delayed;
+ return this;
+ }
+
+ ///
+ public IAmAMessageBuilder SetJobId(Id? jobId)
+ {
+ _jobId = jobId;
+ return this;
+ }
+
+ ///
+ public IAmAMessageBuilder SetMessageId(Id? messageId)
+ {
+ _messageId = messageId;
+ return this;
+ }
+
+ ///
+ public IAmAMessageBuilder SetMessageType(MessageType messageType)
+ {
+ _messageType = messageType;
+ return this;
+ }
+
+ ///
+ public IAmAMessageBuilder SetPartitionKey(PartitionKey partitionKey)
+ {
+ _partitionKey = partitionKey;
+ return this;
+ }
+
+ ///
+ public IAmAMessageBuilder SetReplyTo(RoutingKey? replyTo)
+ {
+ _replyTo = replyTo;
+ return this;
+ }
+
+ ///
+ public IAmAMessageBuilder SetSubject(string? subject)
+ {
+ _subject = subject;
+ return this;
+ }
+
+ ///
+ public IAmAMessageBuilder SetSpecVersion(string? specVersion)
+ {
+ _specVersion = specVersion;
+ return this;
+ }
+
+ ///
+ public IAmAMessageBuilder SetSource(Uri? source)
+ {
+ _source = source;
+ return this;
+ }
+
+ ///
+ public IAmAMessageBuilder SetTopic(RoutingKey topic)
+ {
+ _topic = topic;
+ return this;
+ }
+
+ ///
+ public IAmAMessageBuilder SetTimeStamp(DateTimeOffset timeStamp)
+ {
+ _timeStamp = timeStamp;
+ return this;
+ }
+
+ ///
+ public IAmAMessageBuilder SetTraceParent(TraceParent? traceParent)
+ {
+ _traceParent = traceParent;
+ return this;
+ }
+
+ ///
+ public IAmAMessageBuilder SetTraceState(TraceState? traceState)
+ {
+ _traceState = traceState;
+ return this;
+ }
+
+ ///
+ public IAmAMessageBuilder SetType(CloudEventsType type)
+ {
+ _type = type;
+ return this;
+ }
+
+ ///
+ public IAmAMessageBuilder SetWorkflowId(Id? workflowId)
+ {
+ _workflowId = workflowId;
+ return this;
+ }
+
+ ///
+ public IAmAMessageBuilder SetBody(byte[] body)
+ {
+ _body = body;
+ return this;
+ }
+
+ ///
+ public Message Build()
+ {
+ var messageHeader = new MessageHeader(
+ messageId: _messageId,
+ topic: _topic,
+ messageType: _messageType,
+ source: _source,
+ type: _type,
+ timeStamp: _timeStamp,
+ correlationId: _correlationId,
+ replyTo: _replyTo,
+ contentType: _contentType,
+ partitionKey: _partitionKey,
+ subject: _subject,
+ dataSchema: _dataSchema,
+ delayed: _delayed,
+ traceParent: _traceParent,
+ traceState: _traceState,
+ baggage: _baggage,
+ workflowId: _workflowId,
+ jobId: _jobId)
+ {
+ SpecVersion = _specVersion,
+ Bag = _bag
+ };
+
+ return new Message(messageHeader, new MessageBody(_body));
+ }
+}
diff --git a/tests/Paramore.Brighter.Oracle.Tests/IAmAMessageAssertion.cs b/tests/Paramore.Brighter.Oracle.Tests/IAmAMessageAssertion.cs
new file mode 100644
index 0000000000..76a45c64dc
--- /dev/null
+++ b/tests/Paramore.Brighter.Oracle.Tests/IAmAMessageAssertion.cs
@@ -0,0 +1,43 @@
+#region Licence
+
+/* The MIT License (MIT)
+Copyright © 2014 Ian Cooper
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE. */
+
+#endregion
+
+//
+// This file is auto-generated by Paramore.Brighter.Test.Generator
+//
+
+namespace Paramore.Brighter.Oracle.Tests;
+
+///
+/// Defines a contract for asserting equality between two Message instances.
+///
+public interface IAmAMessageAssertion
+{
+ ///
+ /// Asserts that the actual message matches the expected message.
+ ///
+ /// The expected message.
+ /// The actual message to compare.
+ void Assert(Message expected, Message actual);
+}
diff --git a/tests/Paramore.Brighter.Oracle.Tests/IAmAMessageBuilder.cs b/tests/Paramore.Brighter.Oracle.Tests/IAmAMessageBuilder.cs
new file mode 100644
index 0000000000..30d7e07941
--- /dev/null
+++ b/tests/Paramore.Brighter.Oracle.Tests/IAmAMessageBuilder.cs
@@ -0,0 +1,201 @@
+#region Licence
+
+/* The MIT License (MIT)
+Copyright © 2014 Ian Cooper
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE. */
+
+#endregion
+
+//
+// This file is auto-generated by Paramore.Brighter.Test.Generator
+//
+using System;
+using System.Collections.Generic;
+using System.Net.Mime;
+using Paramore.Brighter.Observability;
+
+namespace Paramore.Brighter.Oracle.Tests;
+
+///
+/// Builder interface for creating test messages with customizable header values and body content.
+///
+public interface IAmAMessageBuilder
+{
+ ///
+ /// Sets the bag of additional header values.
+ ///
+ /// The dictionary of additional header values.
+ /// The builder instance for method chaining.
+ IAmAMessageBuilder SetBag(Dictionary bag);
+
+ ///
+ /// Sets the baggage for distributed tracing context propagation.
+ ///
+ /// The baggage for distributed tracing.
+ /// The builder instance for method chaining.
+ IAmAMessageBuilder SetBaggage(Baggage baggage);
+
+ ///
+ /// Sets the content type of the message body.
+ ///
+ /// The content type of the message.
+ /// The builder instance for method chaining.
+ IAmAMessageBuilder SetContentType(ContentType contentType);
+
+ ///
+ /// Sets the correlation identifier for tracking related messages.
+ ///
+ /// The correlation identifier.
+ /// The builder instance for method chaining.
+ IAmAMessageBuilder SetCorrelationId(Id correlationId);
+
+ ///
+ /// Sets the URI identifying the schema of the data in the message body.
+ ///
+ /// The data schema URI.
+ /// The builder instance for method chaining.
+ IAmAMessageBuilder SetDataSchema(Uri? dataSchema);
+
+ ///
+ /// Sets a reference to external data.
+ ///
+ /// The external data reference.
+ /// The builder instance for method chaining.
+ IAmAMessageBuilder SetDataRef(string? dataRef);
+
+ ///
+ /// Sets the delay before the message should be processed.
+ ///
+ /// The delay timespan.
+ /// The builder instance for method chaining.
+ IAmAMessageBuilder SetDelayed(TimeSpan? delayed);
+
+ ///
+ /// Sets the job identifier for workflow tracking.
+ ///
+ /// The job identifier.
+ /// The builder instance for method chaining.
+ IAmAMessageBuilder SetJobId(Id? jobId);
+
+ ///
+ /// Sets the unique message identifier.
+ ///
+ /// The message identifier.
+ /// The builder instance for method chaining.
+ IAmAMessageBuilder SetMessageId(Id? messageId);
+
+ ///
+ /// Sets the message type (Event, Command, etc.).
+ ///
+ /// The message type.
+ /// The builder instance for method chaining.
+ IAmAMessageBuilder SetMessageType(MessageType messageType);
+
+ ///
+ /// Sets the partition key for message routing.
+ ///
+ /// The partition key.
+ /// The builder instance for method chaining.
+ IAmAMessageBuilder SetPartitionKey(PartitionKey partitionKey);
+
+ ///
+ /// Sets the reply-to routing key for response messages.
+ ///
+ /// The reply-to routing key.
+ /// The builder instance for method chaining.
+ IAmAMessageBuilder SetReplyTo(RoutingKey? replyTo);
+
+ ///
+ /// Sets the subject or title of the message.
+ ///
+ /// The message subject.
+ /// The builder instance for method chaining.
+ IAmAMessageBuilder SetSubject(string? subject);
+
+ ///
+ /// Sets the CloudEvents specification version.
+ ///
+ /// The CloudEvents spec version.
+ /// The builder instance for method chaining.
+ IAmAMessageBuilder SetSpecVersion(string? specVersion);
+
+ ///
+ /// Sets the URI identifying the source of the message.
+ ///
+ /// The message source URI.
+ /// The builder instance for method chaining.
+ IAmAMessageBuilder SetSource(Uri? source);
+
+ ///
+ /// Sets the topic routing key for message publishing.
+ ///
+ /// The topic routing key.
+ /// The builder instance for method chaining.
+ IAmAMessageBuilder SetTopic(RoutingKey topic);
+
+ ///
+ /// Sets the timestamp when the message was created.
+ ///
+ /// The message timestamp.
+ /// The builder instance for method chaining.
+ IAmAMessageBuilder SetTimeStamp(DateTimeOffset timeStamp);
+
+ ///
+ /// Sets the W3C trace parent for distributed tracing.
+ ///
+ /// The W3C trace parent.
+ /// The builder instance for method chaining.
+ IAmAMessageBuilder SetTraceParent(TraceParent? traceParent);
+
+ ///
+ /// Sets the W3C trace state for vendor-specific tracing information.
+ ///
+ /// The W3C trace state.
+ /// The builder instance for method chaining.
+ IAmAMessageBuilder SetTraceState(TraceState? traceState);
+
+ ///
+ /// Sets the CloudEvents type describing the message content.
+ ///
+ /// The CloudEvents type.
+ /// The builder instance for method chaining.
+ IAmAMessageBuilder SetType(CloudEventsType type);
+
+ ///
+ /// Sets the workflow identifier for process tracking.
+ ///
+ /// The workflow identifier.
+ /// The builder instance for method chaining.
+ IAmAMessageBuilder SetWorkflowId(Id? workflowId);
+
+ ///
+ /// Sets the message body content as a byte array.
+ ///
+ /// The message body content.
+ /// The builder instance for method chaining.
+ IAmAMessageBuilder SetBody(byte[] body);
+
+ ///
+ /// Builds and returns the configured message.
+ ///
+ /// The constructed message with all configured properties.
+ Message Build();
+}
+
diff --git a/tests/Paramore.Brighter.Oracle.Tests/Inbox/OracleBinaryInboxAsyncTest.cs b/tests/Paramore.Brighter.Oracle.Tests/Inbox/OracleBinaryInboxAsyncTest.cs
new file mode 100644
index 0000000000..e78cc1853e
--- /dev/null
+++ b/tests/Paramore.Brighter.Oracle.Tests/Inbox/OracleBinaryInboxAsyncTest.cs
@@ -0,0 +1,6 @@
+namespace Paramore.Brighter.Oracle.Tests.Inbox;
+
+public class OracleBinaryInboxAsyncTest : OracleTextInboxAsyncTest
+{
+ protected override bool BinaryMessagePayload => true;
+}
diff --git a/tests/Paramore.Brighter.Oracle.Tests/Inbox/OracleBinaryInboxTest.cs b/tests/Paramore.Brighter.Oracle.Tests/Inbox/OracleBinaryInboxTest.cs
new file mode 100644
index 0000000000..af74edc95d
--- /dev/null
+++ b/tests/Paramore.Brighter.Oracle.Tests/Inbox/OracleBinaryInboxTest.cs
@@ -0,0 +1,6 @@
+namespace Paramore.Brighter.Oracle.Tests.Inbox;
+
+public class OracleBinaryInboxTest : OracleTextInboxTest
+{
+ protected override bool BinaryMessagePayload => true;
+}
diff --git a/tests/Paramore.Brighter.Oracle.Tests/Inbox/OracleJsonInboxTest.cs b/tests/Paramore.Brighter.Oracle.Tests/Inbox/OracleJsonInboxTest.cs
new file mode 100644
index 0000000000..75abc7490c
--- /dev/null
+++ b/tests/Paramore.Brighter.Oracle.Tests/Inbox/OracleJsonInboxTest.cs
@@ -0,0 +1,7 @@
+namespace Paramore.Brighter.Oracle.Tests.Inbox;
+
+public class OracleJsonInboxTest : OracleTextInboxTest
+{
+ protected override bool JsonMessagePayload => true;
+ protected override bool BinaryMessagePayload => true;
+}
diff --git a/tests/Paramore.Brighter.Oracle.Tests/Inbox/OracleTextInboxAsyncTest.cs b/tests/Paramore.Brighter.Oracle.Tests/Inbox/OracleTextInboxAsyncTest.cs
new file mode 100644
index 0000000000..66bedffa91
--- /dev/null
+++ b/tests/Paramore.Brighter.Oracle.Tests/Inbox/OracleTextInboxAsyncTest.cs
@@ -0,0 +1,40 @@
+using System.Threading.Tasks;
+using Oracle.ManagedDataAccess.Client;
+using Paramore.Brighter.Base.Test.Inbox;
+using Paramore.Brighter.Inbox.Oracle;
+
+namespace Paramore.Brighter.Oracle.Tests.Inbox;
+
+public class OracleTextInboxAsyncTest : RelationalDatabaseInboxAsyncTests
+{
+ protected override string DefaultConnectingString => Const.DefaultConnectingString;
+ protected override string TableNamePrefix => Const.TablePrefix;
+ protected override bool BinaryMessagePayload => false;
+ protected override bool JsonMessagePayload => false;
+
+ protected override RelationalDatabaseInbox CreateInbox(RelationalDatabaseConfiguration configuration)
+ {
+ return new OracleInbox(configuration);
+ }
+
+ protected override async Task CreateInboxTableAsync(RelationalDatabaseConfiguration configuration)
+ {
+ await using var connection = new OracleConnection(configuration.ConnectionString);
+ await connection.OpenAsync();
+ await using var command = connection.CreateCommand();
+ command.CommandText = OracleInboxBuilder.GetDDL(
+ configuration.InBoxTableName,
+ BinaryMessagePayload,
+ JsonMessagePayload);
+ await command.ExecuteNonQueryAsync();
+ }
+
+ protected override async Task DeleteInboxTableAsync(RelationalDatabaseConfiguration configuration)
+ {
+ await using var connection = new OracleConnection(configuration.ConnectionString);
+ await connection.OpenAsync();
+ await using var command = connection.CreateCommand();
+ command.CommandText = $"DROP TABLE {configuration.InBoxTableName}";
+ await command.ExecuteNonQueryAsync();
+ }
+}
diff --git a/tests/Paramore.Brighter.Oracle.Tests/Inbox/OracleTextInboxTest.cs b/tests/Paramore.Brighter.Oracle.Tests/Inbox/OracleTextInboxTest.cs
new file mode 100644
index 0000000000..8faef78ab4
--- /dev/null
+++ b/tests/Paramore.Brighter.Oracle.Tests/Inbox/OracleTextInboxTest.cs
@@ -0,0 +1,39 @@
+using Oracle.ManagedDataAccess.Client;
+using Paramore.Brighter.Base.Test.Inbox;
+using Paramore.Brighter.Inbox.Oracle;
+
+namespace Paramore.Brighter.Oracle.Tests.Inbox;
+
+public class OracleTextInboxTest : RelationalDatabaseInboxTests
+{
+ protected override string DefaultConnectingString => Const.DefaultConnectingString;
+ protected override string TableNamePrefix => Const.TablePrefix;
+ protected override bool BinaryMessagePayload => false;
+ protected override bool JsonMessagePayload => false;
+
+ protected override RelationalDatabaseInbox CreateInbox(RelationalDatabaseConfiguration configuration)
+ {
+ return new OracleInbox(configuration);
+ }
+
+ protected override void CreateInboxTable(RelationalDatabaseConfiguration configuration)
+ {
+ using var connection = new OracleConnection(configuration.ConnectionString);
+ connection.Open();
+ using var command = connection.CreateCommand();
+ command.CommandText = OracleInboxBuilder.GetDDL(
+ configuration.InBoxTableName,
+ BinaryMessagePayload,
+ JsonMessagePayload);
+ command.ExecuteNonQuery();
+ }
+
+ protected override void DeleteInboxTable(RelationalDatabaseConfiguration configuration)
+ {
+ using var connection = new OracleConnection(configuration.ConnectionString);
+ connection.Open();
+ using var command = connection.CreateCommand();
+ command.CommandText = $"DROP TABLE {configuration.InBoxTableName}";
+ command.ExecuteNonQuery();
+ }
+}
diff --git a/tests/Paramore.Brighter.Oracle.Tests/Locking/OracleDistributedLockingTest.cs b/tests/Paramore.Brighter.Oracle.Tests/Locking/OracleDistributedLockingTest.cs
new file mode 100644
index 0000000000..cc361a7ad6
--- /dev/null
+++ b/tests/Paramore.Brighter.Oracle.Tests/Locking/OracleDistributedLockingTest.cs
@@ -0,0 +1,14 @@
+using Paramore.Brighter.Base.Test.Locking;
+using Paramore.Brighter.Locking.Oracle;
+
+namespace Paramore.Brighter.Oracle.Tests.Locking;
+
+public class OracleDistributedLockingTest : RelationalDatabaseDistributedLockingAsyncTest
+{
+ protected override string DefaultConnectingString => Const.DefaultConnectingString;
+
+ protected override IDistributedLock CreateDistributedLock()
+ {
+ return new OracleLockingProvider(new OracleConnectionProvider(Configuration));
+ }
+}
diff --git a/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Async/IAmAnOutboxProviderAsync.cs b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Async/IAmAnOutboxProviderAsync.cs
new file mode 100644
index 0000000000..1c9e892028
--- /dev/null
+++ b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Async/IAmAnOutboxProviderAsync.cs
@@ -0,0 +1,68 @@
+#region Licence
+
+/* The MIT License (MIT)
+Copyright © 2014 Ian Cooper
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE. */
+
+#endregion
+
+//
+// This file is auto-generated by Paramore.Brighter.Test.Generator
+//
+
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+namespace Paramore.Brighter.Oracle.Tests.Outbox.Binary.Async;
+
+///
+/// Provider for managing outbox storage and creating outbox instances.
+///
+public interface IAmAnOutboxProviderAsync
+{
+ ///
+ /// Creates the outbox data store.
+ ///
+ Task CreateStoreAsync();
+
+ ///
+ /// Deletes the outbox data store and removes the specified messages.
+ ///
+ /// The messages to remove from the store.
+ Task DeleteStoreAsync(IEnumerable messages);
+
+ ///
+ /// Creates a new outbox instance for storing and retrieving messages.
+ ///
+ /// A new instance.
+ IAmAnOutboxAsync CreateOutboxAsync();
+
+ ///
+ /// Gets all the messages in the outbox.
+ ///
+ /// A list of all messages in the outbox.
+ Task> GetAllMessagesAsync();
+
+ ///
+ /// Creates a new transaction provider.
+ ///
+ /// A new instance.
+ IAmABoxTransactionProvider CreateTransactionProvider();
+}
diff --git a/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Async/When_Adding_A_Duplicate_Message_It_Should_Not_Throw_Async.cs b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Async/When_Adding_A_Duplicate_Message_It_Should_Not_Throw_Async.cs
new file mode 100644
index 0000000000..910231ad22
--- /dev/null
+++ b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Async/When_Adding_A_Duplicate_Message_It_Should_Not_Throw_Async.cs
@@ -0,0 +1,54 @@
+//
+// This file is auto-generated by Paramore.Brighter.Test.Generator
+//
+
+using Xunit;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace Paramore.Brighter.Oracle.Tests.Outbox.Binary.Async;
+
+[Trait("Category", "Oracle")]
+[Collection("OracleBinaryOutbox")]
+public class WhenAddingADuplicateMessageItShouldNotThrowAsync : IAsyncLifetime
+{
+ private readonly IAmAnOutboxProviderAsync _outboxProvider;
+ private readonly IAmAMessageBuilder _messageBuilder;
+ private List _createdMessages = [];
+
+ public WhenAddingADuplicateMessageItShouldNotThrowAsync()
+ {
+ _outboxProvider = new OracleBinaryOutboxProvider();
+ _messageBuilder = new DefaultMessageBuilder();
+ }
+
+ public async Task InitializeAsync()
+ {
+ await _outboxProvider.CreateStoreAsync();
+ }
+
+ public async Task DisposeAsync()
+ {
+ await _outboxProvider.DeleteStoreAsync(_createdMessages);
+ }
+
+ [Fact]
+ public async Task When_Adding_A_Duplicate_Message_It_Should_Not_Throw_Async()
+ {
+ // Arrange
+ var context = new RequestContext();
+ var message = _messageBuilder.Build();
+
+ _createdMessages.Add(message);
+
+ // Act
+ var outbox = _outboxProvider.CreateOutboxAsync();
+ await outbox.AddAsync(message, context);
+
+ // Assert
+ // Just adding a simple assertion to remove any warning
+ Assert.True(true);
+ }
+}
diff --git a/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Async/When_Adding_A_Message_It_Should_Be_Stored_With_All_Properties_Async.cs b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Async/When_Adding_A_Message_It_Should_Be_Stored_With_All_Properties_Async.cs
new file mode 100644
index 0000000000..b40250bfdd
--- /dev/null
+++ b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Async/When_Adding_A_Message_It_Should_Be_Stored_With_All_Properties_Async.cs
@@ -0,0 +1,87 @@
+//
+// This file is auto-generated by Paramore.Brighter.Test.Generator
+//
+
+using Xunit;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace Paramore.Brighter.Oracle.Tests.Outbox.Binary.Async;
+
+[Trait("Category", "Oracle")]
+[Collection("OracleBinaryOutbox")]
+public class WhenAddingAMessageItShouldBeStoredWithAllPropertiesAsync : IAsyncLifetime
+{
+ private readonly IAmAnOutboxProviderAsync _outboxProvider;
+ private readonly IAmAMessageBuilder _messageBuilder;
+ private List _createdMessages = [];
+
+ public WhenAddingAMessageItShouldBeStoredWithAllPropertiesAsync()
+ {
+ _outboxProvider = new OracleBinaryOutboxProvider();
+
+ _messageBuilder = new DefaultMessageBuilder();
+ }
+
+ public async Task InitializeAsync()
+ {
+ await _outboxProvider.CreateStoreAsync();
+ }
+
+ public async Task DisposeAsync()
+ {
+ await _outboxProvider.DeleteStoreAsync(_createdMessages);
+ }
+
+ [Fact]
+ public async Task When_Adding_A_Message_It_Should_Be_Stored_With_All_Properties_Async()
+ {
+ // Arrange
+ var context = new RequestContext();
+ var message = _messageBuilder.Build();
+
+ _createdMessages.Add(message);
+
+ // Act
+ var outbox = _outboxProvider.CreateOutboxAsync();
+ await outbox.AddAsync(message, context);
+
+ var storedMessage = await outbox.GetAsync(message.Id, context);
+
+ // Assert
+ Assert.Equal(message.Body.Value, storedMessage.Body.Value);
+
+ //should read the header from the sql outbox
+ Assert.Equal(message.Header.Topic, storedMessage.Header.Topic);
+ Assert.Equal(message.Header.MessageType, storedMessage.Header.MessageType);
+ Assert.Equal(message.Header.TimeStamp, storedMessage.Header.TimeStamp, TimeSpan.FromSeconds(1));
+ Assert.Equal(0, storedMessage.Header.HandledCount); // -- should be zero when read from outbox
+ Assert.Equal(TimeSpan.Zero, storedMessage.Header.Delayed); // -- should be zero when read from outbox
+ Assert.Equal(message.Header.CorrelationId, storedMessage.Header.CorrelationId);
+ Assert.Equal(message.Header.ReplyTo, storedMessage.Header.ReplyTo);
+ Assert.StartsWith(message.Header.ContentType.ToString(), storedMessage.Header.ContentType.ToString());
+ Assert.Equal(message.Header.PartitionKey, storedMessage.Header.PartitionKey);
+
+ //Bag serialization
+ Assert.Equal(message.Header.Bag.Count, storedMessage.Header.Bag.Count);
+ foreach (var (key, val) in message.Header.Bag)
+ {
+ Assert.Contains(key, storedMessage.Header.Bag);
+ Assert.Equal(val, storedMessage.Header.Bag[key].ToString());
+ }
+
+ //Asserts for workflow properties
+ Assert.Equal(message.Header.WorkflowId, storedMessage.Header.WorkflowId);
+ Assert.Equal(message.Header.JobId, storedMessage.Header.JobId);
+
+ // new fields assertions
+ Assert.Equal(message.Header.Source, storedMessage.Header.Source);
+ Assert.Equal(message.Header.Type, storedMessage.Header.Type);
+ Assert.Equal(message.Header.DataSchema, storedMessage.Header.DataSchema);
+ Assert.Equal(message.Header.Subject, storedMessage.Header.Subject);
+ Assert.Equal(message.Header.TraceParent, storedMessage.Header.TraceParent);
+ Assert.Equal(message.Header.TraceState, storedMessage.Header.TraceState);
+ }
+}
diff --git a/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Async/When_Adding_A_Message_Within_Transaction_And_Rollback_It_Should_Not_Be_Stored_Async.cs b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Async/When_Adding_A_Message_Within_Transaction_And_Rollback_It_Should_Not_Be_Stored_Async.cs
new file mode 100644
index 0000000000..0f408e255c
--- /dev/null
+++ b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Async/When_Adding_A_Message_Within_Transaction_And_Rollback_It_Should_Not_Be_Stored_Async.cs
@@ -0,0 +1,62 @@
+//
+// This file is auto-generated by Paramore.Brighter.Test.Generator
+//
+
+using Xunit;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Oracle.ManagedDataAccess.Client;
+
+namespace Paramore.Brighter.Oracle.Tests.Outbox.Binary.Async;
+
+[Trait("Category", "Oracle")]
+[Collection("OracleBinaryOutbox")]
+public class WhenAddingAMessageWithinTransactionAndRollbackItShouldNotBeStoredAsync : IAsyncLifetime
+{
+ private readonly IAmAnOutboxProviderAsync _outboxProvider;
+ private readonly IAmAMessageBuilder _messageBuilder;
+ private List _createdMessages = [];
+
+ public WhenAddingAMessageWithinTransactionAndRollbackItShouldNotBeStoredAsync()
+ {
+ _outboxProvider = new OracleBinaryOutboxProvider();
+
+ _messageBuilder = new DefaultMessageBuilder();
+ }
+
+ public async Task InitializeAsync()
+ {
+ await _outboxProvider.CreateStoreAsync();
+ }
+
+ public async Task DisposeAsync()
+ {
+ await _outboxProvider.DeleteStoreAsync(_createdMessages);
+ }
+
+ [Fact]
+ public async Task When_Adding_A_Message_Within_Transaction_And_Rollback_It_Should_Not_Be_Stored_Async()
+ {
+ // Arrange
+ var outbox = _outboxProvider.CreateOutboxAsync();
+
+ var transaction = _outboxProvider.CreateTransactionProvider();
+ _ = await transaction.GetTransactionAsync();
+
+ var context = new RequestContext();
+ var message = _messageBuilder.Build();
+
+ _createdMessages.Add(message);
+
+ // Act
+ await outbox.AddAsync(message, context, transactionProvider: transaction);
+ await transaction.RollbackAsync();
+
+ var storedMessage = await outbox.GetAsync(message.Id, context);
+
+ // Assert
+ Assert.Equal(MessageType.MT_NONE, storedMessage.Header.MessageType);
+ }
+}
diff --git a/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Async/When_Adding_A_Message_Within_Transaction_It_Should_Be_Stored_After_Commit_Async.cs b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Async/When_Adding_A_Message_Within_Transaction_It_Should_Be_Stored_After_Commit_Async.cs
new file mode 100644
index 0000000000..23db262140
--- /dev/null
+++ b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Async/When_Adding_A_Message_Within_Transaction_It_Should_Be_Stored_After_Commit_Async.cs
@@ -0,0 +1,92 @@
+//
+// This file is auto-generated by Paramore.Brighter.Test.Generator
+//
+
+using Xunit;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace Paramore.Brighter.Oracle.Tests.Outbox.Binary.Async;
+
+[Trait("Category", "Oracle")]
+[Collection("OracleBinaryOutbox")]
+public class WhenAddingAMessageWithinTransactionItShouldBeStoredAfterCommitAsync : IAsyncLifetime
+{
+ private readonly IAmAnOutboxProviderAsync _outboxProvider;
+ private readonly IAmAMessageBuilder _messageBuilder;
+ private List _createdMessages = [];
+
+ public WhenAddingAMessageWithinTransactionItShouldBeStoredAfterCommitAsync()
+ {
+ _outboxProvider = new OracleBinaryOutboxProvider();
+
+ _messageBuilder = new DefaultMessageBuilder();
+ }
+
+ public async Task InitializeAsync()
+ {
+ await _outboxProvider.CreateStoreAsync();
+ }
+
+ public async Task DisposeAsync()
+ {
+ await _outboxProvider.DeleteStoreAsync(_createdMessages);
+ }
+
+ [Fact]
+ public async Task When_Adding_A_Message_Within_Transaction_It_Should_Be_Stored_After_Commit_Async()
+ {
+ // Arrange
+ var outbox = _outboxProvider.CreateOutboxAsync();
+
+ var transaction = _outboxProvider.CreateTransactionProvider();
+ _ = await transaction.GetTransactionAsync();
+
+ var context = new RequestContext();
+ var message = _messageBuilder.Build();
+
+ _createdMessages.Add(message );
+
+ // Act
+ await outbox.AddAsync(message, context, transactionProvider: transaction);
+ await transaction.CommitAsync();
+
+ var storedMessage = await outbox.GetAsync(message.Id, context);
+
+ // Assert
+ Assert.Equal(message.Body.Value, storedMessage.Body.Value);
+
+ //should read the header from the sql outbox
+ Assert.Equal(message.Header.Topic, storedMessage.Header.Topic);
+ Assert.Equal(message.Header.MessageType, storedMessage.Header.MessageType);
+ Assert.Equal(message.Header.TimeStamp, storedMessage.Header.TimeStamp, TimeSpan.FromSeconds(1));
+ Assert.Equal(0, storedMessage.Header.HandledCount); // -- should be zero when read from outbox
+ Assert.Equal(TimeSpan.Zero, storedMessage.Header.Delayed); // -- should be zero when read from outbox
+ Assert.Equal(message.Header.CorrelationId, storedMessage.Header.CorrelationId);
+ Assert.Equal(message.Header.ReplyTo, storedMessage.Header.ReplyTo);
+ Assert.StartsWith(message.Header.ContentType.ToString(), storedMessage.Header.ContentType.ToString());
+ Assert.Equal(message.Header.PartitionKey, storedMessage.Header.PartitionKey);
+
+ //Bag serialization
+ Assert.Equal(message.Header.Bag.Count, storedMessage.Header.Bag.Count);
+ foreach (var (key, val) in message.Header.Bag)
+ {
+ Assert.Contains(key, storedMessage.Header.Bag);
+ Assert.Equal(val, storedMessage.Header.Bag[key].ToString());
+ }
+
+ //Asserts for workflow properties
+ Assert.Equal(message.Header.WorkflowId, storedMessage.Header.WorkflowId);
+ Assert.Equal(message.Header.JobId, storedMessage.Header.JobId);
+
+ // new fields assertions
+ Assert.Equal(message.Header.Source, storedMessage.Header.Source);
+ Assert.Equal(message.Header.Type, storedMessage.Header.Type);
+ Assert.Equal(message.Header.DataSchema, storedMessage.Header.DataSchema);
+ Assert.Equal(message.Header.Subject, storedMessage.Header.Subject);
+ Assert.Equal(message.Header.TraceParent, storedMessage.Header.TraceParent);
+ Assert.Equal(message.Header.TraceState, storedMessage.Header.TraceState);
+ }
+}
diff --git a/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Async/When_Deleting_Multiple_Messages_They_Should_Be_Removed_From_Outbox_Async.cs b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Async/When_Deleting_Multiple_Messages_They_Should_Be_Removed_From_Outbox_Async.cs
new file mode 100644
index 0000000000..46a718245a
--- /dev/null
+++ b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Async/When_Deleting_Multiple_Messages_They_Should_Be_Removed_From_Outbox_Async.cs
@@ -0,0 +1,65 @@
+//
+// This file is auto-generated by Paramore.Brighter.Test.Generator
+//
+
+using Xunit;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace Paramore.Brighter.Oracle.Tests.Outbox.Binary.Async;
+
+[Trait("Category", "Oracle")]
+[Collection("OracleBinaryOutbox")]
+public class WhenDeletingMultipleMessagesTheyShouldBeRemovedFromOutboxAsync : IAsyncLifetime
+{
+ private readonly IAmAnOutboxProviderAsync _outboxProvider;
+ private List _createdMessages = [];
+
+ public WhenDeletingMultipleMessagesTheyShouldBeRemovedFromOutboxAsync()
+ {
+ _outboxProvider = new OracleBinaryOutboxProvider();
+ }
+
+ public async Task InitializeAsync()
+ {
+ await _outboxProvider.CreateStoreAsync();
+ }
+
+ public async Task DisposeAsync()
+ {
+ await _outboxProvider.DeleteStoreAsync(_createdMessages);
+ }
+
+ [Fact]
+ public async Task When_Deleting_Multiple_Messages_They_Should_Be_Removed_From_Outbox_Async()
+ {
+ // Arrange
+ var context = new RequestContext();
+ var firstMessage = new DefaultMessageBuilder().Build();
+ var secondMessage = new DefaultMessageBuilder().Build();
+ var thirdMessage = new DefaultMessageBuilder().Build();
+
+ _createdMessages.Add(firstMessage);
+ _createdMessages.Add(secondMessage);
+ _createdMessages.Add(thirdMessage);
+
+ // Act
+ var outbox = _outboxProvider.CreateOutboxAsync();
+ await outbox.AddAsync(firstMessage, context);
+ await outbox.AddAsync(secondMessage, context);
+ await outbox.AddAsync(thirdMessage, context);
+
+ await outbox.DeleteAsync([firstMessage.Id, secondMessage.Id, thirdMessage.Id], context);
+
+ // Assert
+ var messages = (await outbox
+ .OutstandingMessagesAsync(TimeSpan.Zero, context))
+ .ToArray();
+
+ Assert.DoesNotContain(firstMessage.Id, messages.Select(x => x.Id));
+ Assert.DoesNotContain(secondMessage.Id, messages.Select(x => x.Id));
+ Assert.DoesNotContain(thirdMessage.Id, messages.Select(x => x.Id));
+ }
+}
diff --git a/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Async/When_Deleting_One_Message_It_Should_Be_Removed_From_Outbox_Async.cs b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Async/When_Deleting_One_Message_It_Should_Be_Removed_From_Outbox_Async.cs
new file mode 100644
index 0000000000..62bb258f5a
--- /dev/null
+++ b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Async/When_Deleting_One_Message_It_Should_Be_Removed_From_Outbox_Async.cs
@@ -0,0 +1,91 @@
+#region Licence
+
+/* The MIT License (MIT)
+Copyright © 2014 Ian Cooper
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE. */
+
+#endregion
+
+//
+// This file is auto-generated by Paramore.Brighter.Test.Generator
+//
+
+using Xunit;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace Paramore.Brighter.Oracle.Tests.Outbox.Binary.Async;
+
+[Trait("Category", "Oracle")]
+[Collection("OracleBinaryOutbox")]
+public class WhenDeletingOneMessageItShouldBeRemovedFromOutboxAsync : IAsyncLifetime
+{
+ private readonly IAmAnOutboxProviderAsync _outboxProvider;
+ private List _createdMessages = [];
+
+ public WhenDeletingOneMessageItShouldBeRemovedFromOutboxAsync()
+ {
+ _outboxProvider = new OracleBinaryOutboxProvider();
+ }
+
+ public async Task InitializeAsync()
+ {
+ await _outboxProvider.CreateStoreAsync();
+ }
+
+ public async Task DisposeAsync()
+ {
+ await _outboxProvider.DeleteStoreAsync(_createdMessages);
+ }
+
+ [Fact]
+ public async Task When_Deleting_One_Message_It_Should_Be_Removed_From_Outbox_Async()
+ {
+ // Arrange
+ var context = new RequestContext();
+
+ var firstMessage = new DefaultMessageBuilder().Build();
+ var secondMessage = new DefaultMessageBuilder().Build();
+ var thirdMessage = new DefaultMessageBuilder().Build();
+
+ _createdMessages.Add(firstMessage);
+ _createdMessages.Add(secondMessage);
+ _createdMessages.Add(thirdMessage);
+
+ // Act
+ var outbox = _outboxProvider.CreateOutboxAsync();
+ await outbox.AddAsync(firstMessage, context);
+ await outbox.AddAsync(secondMessage, context);
+ await outbox.AddAsync(thirdMessage, context);
+
+ await outbox.DeleteAsync([firstMessage.Id], context);
+
+ // Assert
+ var messages = (await outbox
+ .OutstandingMessagesAsync(TimeSpan.Zero, context))
+ .ToArray();
+
+ Assert.DoesNotContain(firstMessage.Id, messages.Select(x => x.Id));
+ Assert.Contains(secondMessage.Id, messages.Select(x => x.Id));
+ Assert.Contains(thirdMessage.Id, messages.Select(x => x.Id));
+ }
+}
diff --git a/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Async/When_Retrieving_A_Message_By_Id_It_Should_Return_The_Correct_Message_Async.cs b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Async/When_Retrieving_A_Message_By_Id_It_Should_Return_The_Correct_Message_Async.cs
new file mode 100644
index 0000000000..07bf9e25b6
--- /dev/null
+++ b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Async/When_Retrieving_A_Message_By_Id_It_Should_Return_The_Correct_Message_Async.cs
@@ -0,0 +1,115 @@
+#region Licence
+
+/* The MIT License (MIT)
+Copyright © 2014 Ian Cooper
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE. */
+
+#endregion
+
+//
+// This file is auto-generated by Paramore.Brighter.Test.Generator
+//
+
+using Xunit;
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+namespace Paramore.Brighter.Oracle.Tests.Outbox.Binary.Async;
+
+[Trait("Category", "Oracle")]
+[Collection("OracleBinaryOutbox")]
+public class WhenRetrievingAMessageByIdItShouldReturnTheCorrectMessageAsync : IAsyncLifetime
+{
+ private readonly IAmAnOutboxProviderAsync _outboxProvider;
+ private List _createdMessages = [];
+
+ public WhenRetrievingAMessageByIdItShouldReturnTheCorrectMessageAsync()
+ {
+ _outboxProvider = new OracleBinaryOutboxProvider();
+ }
+
+ public async Task InitializeAsync()
+ {
+ await _outboxProvider.CreateStoreAsync();
+ }
+
+ public async Task DisposeAsync()
+ {
+ await _outboxProvider.DeleteStoreAsync(_createdMessages);
+ }
+
+ [Fact]
+ public async Task When_Retrieving_A_Message_By_Id_It_Should_Return_The_Correct_Message_Async()
+ {
+ // Arrange
+ var context = new RequestContext();
+ var earliest = new DefaultMessageBuilder().Build();
+ var dispatched = new DefaultMessageBuilder().Build();
+ var undispatched = new DefaultMessageBuilder().Build();
+
+ _createdMessages.Add(earliest);
+ _createdMessages.Add(dispatched);
+ _createdMessages.Add(undispatched);
+
+ var outbox = _outboxProvider.CreateOutboxAsync();
+ await outbox.AddAsync([earliest, dispatched, undispatched], context);
+ await outbox.MarkDispatchedAsync(earliest.Id, context, DateTime.UtcNow.AddHours(-3));
+ await outbox.MarkDispatchedAsync(dispatched.Id, context, DateTime.UtcNow.AddSeconds(-30));
+
+ // Act
+ var message = await outbox.GetAsync(dispatched.Id, context);
+
+ // Assert
+ Assert.NotNull(message);
+ Assert.Equal(message.Body.Value, dispatched.Body.Value);
+
+ //should read the header from the sql outbox
+ Assert.Equal(message.Header.Topic, dispatched.Header.Topic);
+ Assert.Equal(message.Header.MessageType, dispatched.Header.MessageType);
+ Assert.Equal(message.Header.TimeStamp, dispatched.Header.TimeStamp, TimeSpan.FromSeconds(1));
+ Assert.Equal(0, dispatched.Header.HandledCount); // -- should be zero when read from outbox
+ // Assert.Equal(TimeSpan.Zero, dispatched.Header.Delayed); // -- should be zero when read from outbox
+ Assert.Equal(message.Header.CorrelationId, dispatched.Header.CorrelationId);
+ Assert.Equal(message.Header.ReplyTo, dispatched.Header.ReplyTo);
+ Assert.StartsWith(message.Header.ContentType.MediaType, dispatched.Header.ContentType.ToString());
+ Assert.Equal(message.Header.PartitionKey, dispatched.Header.PartitionKey);
+
+ //Bag serialization
+ Assert.Equal(message.Header.Bag.Count, dispatched.Header.Bag.Count);
+ foreach (var (key, val) in message.Header.Bag)
+ {
+ Assert.Contains(key, dispatched.Header.Bag);
+ Assert.Equal(val.ToString(), dispatched.Header.Bag[key].ToString());
+ }
+
+ //Asserts for workflow properties
+ Assert.Equal(message.Header.WorkflowId, dispatched.Header.WorkflowId);
+ Assert.Equal(message.Header.JobId, dispatched.Header.JobId);
+
+ // new fields assertions
+ Assert.Equal(message.Header.Source, dispatched.Header.Source);
+ Assert.Equal(message.Header.Type, dispatched.Header.Type);
+ Assert.Equal(message.Header.DataSchema, dispatched.Header.DataSchema);
+ Assert.Equal(message.Header.Subject, dispatched.Header.Subject);
+ Assert.Equal(message.Header.TraceParent, dispatched.Header.TraceParent);
+ Assert.Equal(message.Header.TraceState, dispatched.Header.TraceState);
+ }
+}
diff --git a/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Async/When_Retrieving_A_Non_Existent_Message_It_Should_Return_Empty_Message_Async.cs b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Async/When_Retrieving_A_Non_Existent_Message_It_Should_Return_Empty_Message_Async.cs
new file mode 100644
index 0000000000..3a5aecb18e
--- /dev/null
+++ b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Async/When_Retrieving_A_Non_Existent_Message_It_Should_Return_Empty_Message_Async.cs
@@ -0,0 +1,52 @@
+//
+// This file is auto-generated by Paramore.Brighter.Test.Generator
+//
+
+using Xunit;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace Paramore.Brighter.Oracle.Tests.Outbox.Binary.Async;
+
+[Trait("Category", "Oracle")]
+[Collection("OracleBinaryOutbox")]
+public class WhenRetrievingANonExistentMessageItShouldReturnEmptyMessageAsync : IAsyncLifetime
+{
+ private readonly IAmAnOutboxProviderAsync _outboxProvider;
+ private readonly IAmAMessageBuilder _messageBuilder;
+ private List _createdMessages = [];
+
+ public WhenRetrievingANonExistentMessageItShouldReturnEmptyMessageAsync()
+ {
+ _outboxProvider = new OracleBinaryOutboxProvider();
+
+ _messageBuilder = new DefaultMessageBuilder();
+ }
+
+ public async Task InitializeAsync()
+ {
+ await _outboxProvider.CreateStoreAsync();
+ }
+
+ public async Task DisposeAsync()
+ {
+ await _outboxProvider.DeleteStoreAsync(_createdMessages);
+ }
+
+ [Fact]
+ public async Task When_Retrieving_A_Non_Existent_Message_It_Should_Return_Empty_Message_Async()
+ {
+ // Arrange
+ var context = new RequestContext();
+
+ var outbox = _outboxProvider.CreateOutboxAsync();
+
+ // Act
+ var message = await outbox.GetAsync(Id.Random(), context);
+
+ // Assert
+ Assert.Equal(MessageType.MT_NONE, message.Header.MessageType);
+ }
+}
diff --git a/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Async/When_Retrieving_All_Messages_They_Should_Include_Dispatched_And_Undispatched_Async.cs b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Async/When_Retrieving_All_Messages_They_Should_Include_Dispatched_And_Undispatched_Async.cs
new file mode 100644
index 0000000000..3c23e7e3bd
--- /dev/null
+++ b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Async/When_Retrieving_All_Messages_They_Should_Include_Dispatched_And_Undispatched_Async.cs
@@ -0,0 +1,87 @@
+#region Licence
+
+/* The MIT License (MIT)
+Copyright © 2014 Ian Cooper
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE. */
+
+#endregion
+
+//
+// This file is auto-generated by Paramore.Brighter.Test.Generator
+//
+
+using Xunit;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace Paramore.Brighter.Oracle.Tests.Outbox.Binary.Async;
+
+[Trait("Category", "Oracle")]
+[Collection("OracleBinaryOutbox")]
+public class WhenRetrievingAllMessagesTheyShouldIncludeDispatchedAndUndispatchedAsync : IAsyncLifetime
+{
+ private readonly IAmAnOutboxProviderAsync _outboxProvider;
+ private List _createdMessages = [];
+
+ public WhenRetrievingAllMessagesTheyShouldIncludeDispatchedAndUndispatchedAsync()
+ {
+ _outboxProvider = new OracleBinaryOutboxProvider();
+ }
+
+ public async Task InitializeAsync()
+ {
+ await _outboxProvider.CreateStoreAsync();
+ }
+
+ public async Task DisposeAsync()
+ {
+ await _outboxProvider.DeleteStoreAsync(_createdMessages);
+ }
+
+ [Fact]
+ public async Task When_Retrieving_All_Messages_They_Should_Include_Dispatched_And_Undispatched_Async()
+ {
+ // Arrange
+ var context = new RequestContext();
+ var earliest = new DefaultMessageBuilder().Build();
+ var dispatched = new DefaultMessageBuilder().Build();
+ var undispatched = new DefaultMessageBuilder().Build();
+
+ _createdMessages.Add(earliest);
+ _createdMessages.Add(dispatched);
+ _createdMessages.Add(undispatched);
+
+ var outbox = _outboxProvider.CreateOutboxAsync();
+ await outbox.AddAsync([earliest, dispatched, undispatched], context);
+ await outbox.MarkDispatchedAsync(earliest.Id, context, DateTime.UtcNow.AddHours(-3));
+ await outbox.MarkDispatchedAsync(dispatched.Id, context, DateTime.UtcNow.AddSeconds(-30));
+
+ // Act
+ var messages = (await _outboxProvider.GetAllMessagesAsync()).ToArray();
+
+ // Assert
+ Assert.True(messages.Length >= 3, "Expecting at least 3 messages");
+ Assert.Contains(earliest.Id, messages.Select(x => x.Id));
+ Assert.Contains(dispatched.Id, messages.Select(x => x.Id));
+ Assert.Contains(undispatched.Id, messages.Select(x => x.Id));
+ }
+}
diff --git a/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Async/When_Retrieving_Dispatched_Messages_It_Should_Filter_By_Age_Async.cs b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Async/When_Retrieving_Dispatched_Messages_It_Should_Filter_By_Age_Async.cs
new file mode 100644
index 0000000000..14e5cc7c61
--- /dev/null
+++ b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Async/When_Retrieving_Dispatched_Messages_It_Should_Filter_By_Age_Async.cs
@@ -0,0 +1,73 @@
+//
+// This file is auto-generated by Paramore.Brighter.Test.Generator
+//
+
+using Xunit;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace Paramore.Brighter.Oracle.Tests.Outbox.Binary.Async;
+
+[Trait("Category", "Oracle")]
+[Collection("OracleBinaryOutbox")]
+public class WhenRetrievingDispatchedMessagesItShouldFilterByAgeAsync : IAsyncLifetime
+{
+ private readonly IAmAnOutboxProviderAsync _outboxProvider;
+ private List _createdMessages = [];
+
+ public WhenRetrievingDispatchedMessagesItShouldFilterByAgeAsync()
+ {
+ _outboxProvider = new OracleBinaryOutboxProvider();
+ }
+
+ public async Task InitializeAsync()
+ {
+ await _outboxProvider.CreateStoreAsync();
+ }
+
+ public async Task DisposeAsync()
+ {
+ await _outboxProvider.DeleteStoreAsync(_createdMessages);
+ }
+
+ [Fact]
+ public async Task When_Retrieving_Dispatched_Messages_It_Should_Filter_By_Age_Async()
+ {
+ // Arrange
+ var context = new RequestContext();
+ var earliest = new DefaultMessageBuilder().Build();
+ var dispatched = new DefaultMessageBuilder().Build();
+ var undispatched = new DefaultMessageBuilder().Build();
+
+ _createdMessages.Add(earliest);
+ _createdMessages.Add(dispatched);
+ _createdMessages.Add(undispatched);
+
+ var outbox = _outboxProvider.CreateOutboxAsync();
+ await outbox.AddAsync([earliest, dispatched, undispatched], context);
+ await outbox.MarkDispatchedAsync(earliest.Id, context, DateTime.UtcNow.AddHours(-3));
+ await outbox.MarkDispatchedAsync(dispatched.Id, context, DateTime.UtcNow.AddSeconds(-30));
+
+ // Act
+ var allDispatched = (await outbox.DispatchedMessagesAsync(TimeSpan.Zero, context)).ToArray();
+ var messagesOverAnHour = (await outbox.DispatchedMessagesAsync(TimeSpan.FromHours(1), context)).ToArray();
+ var messagesOver4Hours = (await outbox.DispatchedMessagesAsync(TimeSpan.FromHours(4), context)).ToArray();
+
+ // Assert
+ Assert.True(allDispatched.Length >= 2, "Expecting at least 2 messages");
+ Assert.Contains(earliest.Id, allDispatched.Select(x => x.Id));
+ Assert.Contains(dispatched.Id, allDispatched.Select(x => x.Id));
+ Assert.DoesNotContain(undispatched.Id, allDispatched.Select(x => x.Id));
+
+ Assert.True(messagesOverAnHour.Length >= 1, "Expecting at least 1 message");
+ Assert.Contains(earliest.Id, messagesOverAnHour.Select(x => x.Id));
+ Assert.DoesNotContain(dispatched.Id, messagesOverAnHour.Select(x => x.Id));
+ Assert.DoesNotContain(undispatched.Id, messagesOverAnHour.Select(x => x.Id));
+
+ Assert.DoesNotContain(earliest.Id, messagesOver4Hours.Select(x => x.Id));
+ Assert.DoesNotContain(dispatched.Id, messagesOver4Hours.Select(x => x.Id));
+ Assert.DoesNotContain(undispatched.Id, messagesOver4Hours.Select(x => x.Id));
+ }
+}
diff --git a/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Async/When_Retrieving_Messages_By_Ids_It_Should_Return_Only_Requested_Messages_Async.cs b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Async/When_Retrieving_Messages_By_Ids_It_Should_Return_Only_Requested_Messages_Async.cs
new file mode 100644
index 0000000000..92a9e8b50c
--- /dev/null
+++ b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Async/When_Retrieving_Messages_By_Ids_It_Should_Return_Only_Requested_Messages_Async.cs
@@ -0,0 +1,89 @@
+#region Licence
+
+/* The MIT License (MIT)
+Copyright © 2014 Ian Cooper
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE. */
+
+#endregion
+
+//
+// This file is auto-generated by Paramore.Brighter.Test.Generator
+//
+
+using Xunit;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace Paramore.Brighter.Oracle.Tests.Outbox.Binary.Async;
+
+[Trait("Category", "Oracle")]
+[Collection("OracleBinaryOutbox")]
+public class WhenRetrievingMessagesByIdsItShouldReturnOnlyRequestedMessagesAsync : IAsyncLifetime
+{
+ private readonly IAmAnOutboxProviderAsync _outboxProvider;
+ private List _createdMessages = [];
+
+ public WhenRetrievingMessagesByIdsItShouldReturnOnlyRequestedMessagesAsync()
+ {
+ _outboxProvider = new OracleBinaryOutboxProvider();
+ }
+
+ public async Task InitializeAsync()
+ {
+ await _outboxProvider.CreateStoreAsync();
+ }
+
+ public async Task DisposeAsync()
+ {
+ await _outboxProvider.DeleteStoreAsync(_createdMessages);
+ }
+
+ [Fact]
+ public async Task When_Retrieving_Messages_By_Ids_It_Should_Return_Only_Requested_Messages_Async()
+ {
+ // Arrange
+ var context = new RequestContext();
+ var earliest = new DefaultMessageBuilder().Build();
+ var dispatched = new DefaultMessageBuilder().Build();
+ var undispatched = new DefaultMessageBuilder().Build();
+
+ _createdMessages.Add(earliest);
+ _createdMessages.Add(dispatched);
+ _createdMessages.Add(undispatched);
+
+ var outbox = _outboxProvider.CreateOutboxAsync();
+ await outbox.AddAsync([earliest, dispatched, undispatched], context);
+ await outbox.MarkDispatchedAsync(earliest.Id, context, DateTime.UtcNow.AddHours(-3));
+ await outbox.MarkDispatchedAsync(dispatched.Id, context, DateTime.UtcNow.AddSeconds(-30));
+
+ // Act
+ var messages = (await outbox
+ .GetAsync([earliest.Id, undispatched.Id], context))
+ .ToArray();
+
+ // Assert
+ Assert.Equal(2, messages.Length);
+ Assert.Contains(earliest.Id, messages.Select(x => x.Id));
+ Assert.DoesNotContain(dispatched.Id, messages.Select(x => x.Id));
+ Assert.Contains(undispatched.Id, messages.Select(x => x.Id));
+ }
+}
diff --git a/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Async/When_Retrieving_Outstanding_Messages_It_Should_Filter_By_Age_Async.cs b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Async/When_Retrieving_Outstanding_Messages_It_Should_Filter_By_Age_Async.cs
new file mode 100644
index 0000000000..f1bc0e5149
--- /dev/null
+++ b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Async/When_Retrieving_Outstanding_Messages_It_Should_Filter_By_Age_Async.cs
@@ -0,0 +1,73 @@
+//
+// This file is auto-generated by Paramore.Brighter.Test.Generator
+//
+
+using Xunit;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace Paramore.Brighter.Oracle.Tests.Outbox.Binary.Async;
+
+[Trait("Category", "Oracle")]
+[Collection("OracleBinaryOutbox")]
+public class WhenRetrievingOutstandingMessagesItShouldFilterByAgeAsync : IAsyncLifetime
+{
+ private readonly IAmAnOutboxProviderAsync _outboxProvider;
+ private List _createdMessages = [];
+
+ public WhenRetrievingOutstandingMessagesItShouldFilterByAgeAsync()
+ {
+ _outboxProvider = new OracleBinaryOutboxProvider();
+ }
+
+ public async Task InitializeAsync()
+ {
+ await _outboxProvider.CreateStoreAsync();
+ }
+
+ public async Task DisposeAsync()
+ {
+ await _outboxProvider.DeleteStoreAsync(_createdMessages);
+ }
+
+ [Fact]
+ public async Task When_Retrieving_Outstanding_Messages_It_Should_Filter_By_Age_Async()
+ {
+ // Arrange
+ var context = new RequestContext();
+ var earliest = new DefaultMessageBuilder().Build();
+ var dispatched = new DefaultMessageBuilder().Build();
+ var undispatched = new DefaultMessageBuilder().Build();
+
+ _createdMessages.Add(earliest);
+ _createdMessages.Add(dispatched);
+ _createdMessages.Add(undispatched);
+
+ var outbox = _outboxProvider.CreateOutboxAsync();
+ await outbox.AddAsync([earliest, dispatched, undispatched], context);
+ await outbox.MarkDispatchedAsync(earliest.Id, context, DateTime.UtcNow.AddHours(-3));
+ await outbox.MarkDispatchedAsync(dispatched.Id, context, DateTime.UtcNow.AddSeconds(-30));
+
+ // Act
+ var allDispatched = (await outbox.DispatchedMessagesAsync(TimeSpan.Zero, context)).ToArray();
+ var messagesOverAnHour = (await outbox.DispatchedMessagesAsync(TimeSpan.FromHours(1), context)).ToArray();
+ var messagesOver4Hours = (await outbox.DispatchedMessagesAsync(TimeSpan.FromHours(4), context)).ToArray();
+
+ // Assert
+ Assert.True(allDispatched.Length >= 2, "Expecting at least 2 messages");
+ Assert.Contains(earliest.Id, allDispatched.Select(x => x.Id));
+ Assert.Contains(dispatched.Id, allDispatched.Select(x => x.Id));
+ Assert.DoesNotContain(undispatched.Id, allDispatched.Select(x => x.Id));
+
+ Assert.True(messagesOverAnHour.Length >= 1, "Expecting at least 1 message");
+ Assert.Contains(earliest.Id, messagesOverAnHour.Select(x => x.Id));
+ Assert.DoesNotContain(dispatched.Id, messagesOverAnHour.Select(x => x.Id));
+ Assert.DoesNotContain(undispatched.Id, messagesOverAnHour.Select(x => x.Id));
+
+ Assert.DoesNotContain(earliest.Id, messagesOver4Hours.Select(x => x.Id));
+ Assert.DoesNotContain(dispatched.Id, messagesOver4Hours.Select(x => x.Id));
+ Assert.DoesNotContain(undispatched.Id, messagesOver4Hours.Select(x => x.Id));
+ }
+}
diff --git a/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Sync/IAmAnOutboxProviderSync.cs b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Sync/IAmAnOutboxProviderSync.cs
new file mode 100644
index 0000000000..f13cff19c4
--- /dev/null
+++ b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Sync/IAmAnOutboxProviderSync.cs
@@ -0,0 +1,67 @@
+#region Licence
+
+/* The MIT License (MIT)
+Copyright © 2014 Ian Cooper
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE. */
+
+#endregion
+
+//
+// This file is auto-generated by Paramore.Brighter.Test.Generator
+//
+
+using System.Collections.Generic;
+
+namespace Paramore.Brighter.Oracle.Tests.Outbox.Binary.Sync;
+
+///
+/// Provider for managing outbox storage and creating outbox instances.
+///
+public interface IAmAnOutboxProviderSync
+{
+ ///
+ /// Creates the outbox data store.
+ ///
+ void CreateStore();
+
+ ///
+ /// Deletes the outbox data store and removes the specified messages.
+ ///
+ /// The messages to remove from the store.
+ void DeleteStore(IEnumerable messages);
+
+ ///
+ /// Creates a new outbox instance for storing and retrieving messages.
+ ///
+ /// A new instance.
+ IAmAnOutboxSync CreateOutbox();
+
+ ///
+ /// Gets all the messages in the outbox.
+ ///
+ /// A list of all messages in the outbox.
+ IEnumerable GetAllMessages();
+
+ ///
+ /// Creates a new transaction provider.
+ ///
+ /// A new instance.
+ IAmABoxTransactionProvider CreateTransactionProvider();
+}
diff --git a/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Sync/When_Adding_A_Duplicate_Message_It_Should_Not_Throw.cs b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Sync/When_Adding_A_Duplicate_Message_It_Should_Not_Throw.cs
new file mode 100644
index 0000000000..925d4c84b0
--- /dev/null
+++ b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Sync/When_Adding_A_Duplicate_Message_It_Should_Not_Throw.cs
@@ -0,0 +1,73 @@
+#region Licence
+
+/* The MIT License (MIT)
+Copyright © 2014 Ian Cooper
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE. */
+
+#endregion
+
+//
+// This file is auto-generated by Paramore.Brighter.Test.Generator
+//
+
+using Xunit;
+using System;
+using System.Collections.Generic;
+
+namespace Paramore.Brighter.Oracle.Tests.Outbox.Binary.Sync;
+
+[Trait("Category", "Oracle")]
+public class WhenAddingADuplicateMessageItShouldNotThrow : IDisposable
+{
+ private readonly IAmAnOutboxProviderSync _outboxProvider;
+ private readonly IAmAMessageBuilder _messageBuilder;
+ private List _createdMessages = [];
+
+ public WhenAddingADuplicateMessageItShouldNotThrow()
+ {
+ _outboxProvider = new OracleBinaryOutboxProvider();
+ _outboxProvider.CreateStore();
+
+ _messageBuilder = new DefaultMessageBuilder();
+ }
+
+ [Fact]
+ public void When_Adding_A_Duplicate_Message_It_Should_Not_Throw()
+ {
+ // Arrange
+ var context = new RequestContext();
+ var message = _messageBuilder.Build();
+
+ _createdMessages.Add(message);
+
+ // Act
+ var outbox = _outboxProvider.CreateOutbox();
+ outbox.Add(message, context);
+
+ // Assert
+ // Just adding a simple assertion to remove any warning
+ Assert.True(true);
+ }
+
+ public void Dispose()
+ {
+ _outboxProvider.DeleteStore(_createdMessages);
+ }
+}
diff --git a/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Sync/When_Adding_A_Message_It_Should_Be_Stored_With_All_Properties.cs b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Sync/When_Adding_A_Message_It_Should_Be_Stored_With_All_Properties.cs
new file mode 100644
index 0000000000..52fd7a5a7c
--- /dev/null
+++ b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Sync/When_Adding_A_Message_It_Should_Be_Stored_With_All_Properties.cs
@@ -0,0 +1,105 @@
+#region Licence
+
+/* The MIT License (MIT)
+Copyright © 2014 Ian Cooper
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE. */
+
+#endregion
+
+//
+// This file is auto-generated by Paramore.Brighter.Test.Generator
+//
+
+using Xunit;
+using System;
+using System.Collections.Generic;
+
+namespace Paramore.Brighter.Oracle.Tests.Outbox.Binary.Sync;
+
+[Trait("Category", "Oracle")]
+public class WhenAddingAMessageItShouldBeStoredWithAllProperties : IDisposable
+{
+ private readonly IAmAnOutboxProviderSync _outboxProvider;
+ private readonly IAmAMessageBuilder _messageBuilder;
+ private List _createdMessages = [];
+
+ public WhenAddingAMessageItShouldBeStoredWithAllProperties()
+ {
+ _outboxProvider = new OracleBinaryOutboxProvider();
+ _outboxProvider.CreateStore();
+
+ _messageBuilder = new DefaultMessageBuilder();
+ }
+
+ [Fact]
+ public void When_Adding_A_Message_It_Should_Be_Stored_With_All_Properties()
+ {
+ // Arrange
+ var context = new RequestContext();
+ var message = _messageBuilder.Build();
+
+ _createdMessages.Add(message);
+
+ // Act
+ var outbox = _outboxProvider.CreateOutbox();
+ outbox.Add(message, context);
+
+ var storedMessage = outbox.Get(message.Id, context);
+
+ // Assert
+ Assert.Equal(message.Body.Value, storedMessage.Body.Value);
+
+ //should read the header from the sql outbox
+ Assert.Equal(message.Header.Topic, storedMessage.Header.Topic);
+ Assert.Equal(message.Header.MessageType, storedMessage.Header.MessageType);
+ Assert.Equal(message.Header.TimeStamp, storedMessage.Header.TimeStamp, TimeSpan.FromSeconds(1));
+ Assert.Equal(0, storedMessage.Header.HandledCount); // -- should be zero when read from outbox
+ Assert.Equal(TimeSpan.Zero, storedMessage.Header.Delayed); // -- should be zero when read from outbox
+ Assert.Equal(message.Header.CorrelationId, storedMessage.Header.CorrelationId);
+ Assert.Equal(message.Header.ReplyTo, storedMessage.Header.ReplyTo);
+ Assert.StartsWith(message.Header.ContentType.ToString(), storedMessage.Header.ContentType.ToString());
+ Assert.Equal(message.Header.PartitionKey, storedMessage.Header.PartitionKey);
+
+ //Bag serialization
+ Assert.Equal(message.Header.Bag.Count, storedMessage.Header.Bag.Count);
+ foreach (var (key, val) in message.Header.Bag)
+ {
+ Assert.Contains(key, storedMessage.Header.Bag);
+ Assert.Equal(val, storedMessage.Header.Bag[key].ToString());
+ }
+
+ //Asserts for workflow properties
+ Assert.Equal(message.Header.WorkflowId, storedMessage.Header.WorkflowId);
+ Assert.Equal(message.Header.JobId, storedMessage.Header.JobId);
+
+ // new fields assertions
+ Assert.Equal(message.Header.Source, storedMessage.Header.Source);
+ Assert.Equal(message.Header.Type, storedMessage.Header.Type);
+ Assert.Equal(message.Header.DataSchema, storedMessage.Header.DataSchema);
+ Assert.Equal(message.Header.Subject, storedMessage.Header.Subject);
+ Assert.Equal(message.Header.TraceParent, storedMessage.Header.TraceParent);
+ Assert.Equal(message.Header.TraceState, storedMessage.Header.TraceState);
+ }
+
+ public void Dispose()
+ {
+ _outboxProvider.DeleteStore(_createdMessages);
+ }
+}
diff --git a/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Sync/When_Adding_A_Message_Within_Transaction_And_Rollback_It_Should_Not_Be_Stored.cs b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Sync/When_Adding_A_Message_Within_Transaction_And_Rollback_It_Should_Not_Be_Stored.cs
new file mode 100644
index 0000000000..b34bf19e08
--- /dev/null
+++ b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Sync/When_Adding_A_Message_Within_Transaction_And_Rollback_It_Should_Not_Be_Stored.cs
@@ -0,0 +1,79 @@
+#region Licence
+
+/* The MIT License (MIT)
+Copyright © 2014 Ian Cooper
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE. */
+
+#endregion
+
+//
+// This file is auto-generated by Paramore.Brighter.Test.Generator
+//
+
+using Xunit;
+using System;
+using System.Collections.Generic;
+
+namespace Paramore.Brighter.Oracle.Tests.Outbox.Binary.Sync;
+
+[Trait("Category", "Oracle")]
+public class WhenAddingAMessageWithinTransactionAndRollbackItShouldNotBeStored : IDisposable
+{
+ private readonly IAmAnOutboxProviderSync _outboxProvider;
+ private readonly IAmAMessageBuilder _messageBuilder;
+ private List _createdMessages = [];
+
+ public WhenAddingAMessageWithinTransactionAndRollbackItShouldNotBeStored()
+ {
+ _outboxProvider = new OracleBinaryOutboxProvider();
+ _outboxProvider.CreateStore();
+
+ _messageBuilder = new DefaultMessageBuilder();
+ }
+
+ [Fact]
+ public void When_Adding_A_Message_Within_Transaction_And_Rollback_It_Should_Not_Be_Stored()
+ {
+ // Arrange
+ var outbox = _outboxProvider.CreateOutbox();
+
+ var transaction = _outboxProvider.CreateTransactionProvider();
+ _ = transaction.GetTransaction();
+
+ var context = new RequestContext();
+ var message = _messageBuilder.Build();
+
+ _createdMessages.Add(message);
+
+ // Act
+ outbox.Add(message, context, transactionProvider: transaction);
+ transaction.Rollback();
+
+ var storedMessage = outbox.Get(message.Id, context);
+
+ // Assert
+ Assert.Equal(MessageType.MT_NONE, storedMessage.Header.MessageType);
+ }
+
+ public void Dispose()
+ {
+ _outboxProvider.DeleteStore(_createdMessages);
+ }
+}
diff --git a/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Sync/When_Adding_A_Message_Within_Transaction_It_Should_Be_Stored_After_Commit.cs b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Sync/When_Adding_A_Message_Within_Transaction_It_Should_Be_Stored_After_Commit.cs
new file mode 100644
index 0000000000..037a50065e
--- /dev/null
+++ b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Sync/When_Adding_A_Message_Within_Transaction_It_Should_Be_Stored_After_Commit.cs
@@ -0,0 +1,110 @@
+#region Licence
+
+/* The MIT License (MIT)
+Copyright © 2014 Ian Cooper
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE. */
+
+#endregion
+
+//
+// This file is auto-generated by Paramore.Brighter.Test.Generator
+//
+
+using Xunit;
+using System;
+using System.Collections.Generic;
+
+namespace Paramore.Brighter.Oracle.Tests.Outbox.Binary.Sync;
+
+[Trait("Category", "Oracle")]
+public class WhenAddingAMessageWithinTransactionItShouldBeStoredAfterCommit : IDisposable
+{
+ private readonly IAmAnOutboxProviderSync _outboxProvider;
+ private readonly IAmAMessageBuilder _messageBuilder;
+ private List _createdMessages = [];
+
+ public WhenAddingAMessageWithinTransactionItShouldBeStoredAfterCommit()
+ {
+ _outboxProvider = new OracleBinaryOutboxProvider();
+ _outboxProvider.CreateStore();
+
+ _messageBuilder = new DefaultMessageBuilder();
+ }
+
+ [Fact]
+ public void When_Adding_A_Message_Within_Transaction_It_Should_Be_Stored_After_Commit()
+ {
+ // Arrange
+ var outbox = _outboxProvider.CreateOutbox();
+
+ var transaction = _outboxProvider.CreateTransactionProvider();
+ _ = transaction.GetTransaction();
+
+ var context = new RequestContext();
+ var message = _messageBuilder.Build();
+
+ _createdMessages.Add(message );
+
+ // Act
+ outbox.Add(message, context, transactionProvider: transaction);
+ transaction.Commit();
+
+ var storedMessage = outbox.Get(message.Id, context);
+
+ // Assert
+ Assert.Equal(message.Body.Value, storedMessage.Body.Value);
+
+ //should read the header from the sql outbox
+ Assert.Equal(message.Header.Topic, storedMessage.Header.Topic);
+ Assert.Equal(message.Header.MessageType, storedMessage.Header.MessageType);
+ Assert.Equal(message.Header.TimeStamp, storedMessage.Header.TimeStamp, TimeSpan.FromSeconds(1));
+ Assert.Equal(0, storedMessage.Header.HandledCount); // -- should be zero when read from outbox
+ Assert.Equal(TimeSpan.Zero, storedMessage.Header.Delayed); // -- should be zero when read from outbox
+ Assert.Equal(message.Header.CorrelationId, storedMessage.Header.CorrelationId);
+ Assert.Equal(message.Header.ReplyTo, storedMessage.Header.ReplyTo);
+ Assert.StartsWith(message.Header.ContentType.ToString(), storedMessage.Header.ContentType.ToString());
+ Assert.Equal(message.Header.PartitionKey, storedMessage.Header.PartitionKey);
+
+ //Bag serialization
+ Assert.Equal(message.Header.Bag.Count, storedMessage.Header.Bag.Count);
+ foreach (var (key, val) in message.Header.Bag)
+ {
+ Assert.Contains(key, storedMessage.Header.Bag);
+ Assert.Equal(val, storedMessage.Header.Bag[key].ToString());
+ }
+
+ //Asserts for workflow properties
+ Assert.Equal(message.Header.WorkflowId, storedMessage.Header.WorkflowId);
+ Assert.Equal(message.Header.JobId, storedMessage.Header.JobId);
+
+ // new fields assertions
+ Assert.Equal(message.Header.Source, storedMessage.Header.Source);
+ Assert.Equal(message.Header.Type, storedMessage.Header.Type);
+ Assert.Equal(message.Header.DataSchema, storedMessage.Header.DataSchema);
+ Assert.Equal(message.Header.Subject, storedMessage.Header.Subject);
+ Assert.Equal(message.Header.TraceParent, storedMessage.Header.TraceParent);
+ Assert.Equal(message.Header.TraceState, storedMessage.Header.TraceState);
+ }
+
+ public void Dispose()
+ {
+ _outboxProvider.DeleteStore(_createdMessages);
+ }
+}
diff --git a/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Sync/When_Deleting_Multiple_Messages_They_Should_Be_Removed_From_Outbox.cs b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Sync/When_Deleting_Multiple_Messages_They_Should_Be_Removed_From_Outbox.cs
new file mode 100644
index 0000000000..b7e5d929d3
--- /dev/null
+++ b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Sync/When_Deleting_Multiple_Messages_They_Should_Be_Removed_From_Outbox.cs
@@ -0,0 +1,84 @@
+#region Licence
+
+/* The MIT License (MIT)
+Copyright © 2014 Ian Cooper
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE. */
+
+#endregion
+
+//
+// This file is auto-generated by Paramore.Brighter.Test.Generator
+//
+
+using Xunit;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Paramore.Brighter.Oracle.Tests.Outbox.Binary.Sync;
+
+[Trait("Category", "Oracle")]
+public class WhenDeletingMultipleMessagesTheyShouldBeRemovedFromOutbox : IDisposable
+{
+ private readonly IAmAnOutboxProviderSync _outboxProvider;
+ private List _createdMessages = [];
+
+ public WhenDeletingMultipleMessagesTheyShouldBeRemovedFromOutbox()
+ {
+ _outboxProvider = new OracleBinaryOutboxProvider();
+ _outboxProvider.CreateStore();
+ }
+
+ [Fact]
+ public void When_Deleting_Multiple_Messages_They_Should_Be_Removed_From_Outbox()
+ {
+ // Arrange
+ var context = new RequestContext();
+ var firstMessage = new DefaultMessageBuilder().Build();
+ var secondMessage = new DefaultMessageBuilder().Build();
+ var thirdMessage = new DefaultMessageBuilder().Build();
+
+ _createdMessages.Add(firstMessage);
+ _createdMessages.Add(secondMessage);
+ _createdMessages.Add(thirdMessage);
+
+ // Act
+ var outbox = _outboxProvider.CreateOutbox();
+ outbox.Add(firstMessage, context);
+ outbox.Add(secondMessage, context);
+ outbox.Add(thirdMessage, context);
+
+ outbox.Delete([firstMessage.Id, secondMessage.Id, thirdMessage.Id], context);
+
+ // Assert
+ var messages = outbox
+ .OutstandingMessages(TimeSpan.Zero, context)
+ .ToArray();
+
+ Assert.DoesNotContain(firstMessage.Id, messages.Select(x => x.Id));
+ Assert.DoesNotContain(secondMessage.Id, messages.Select(x => x.Id));
+ Assert.DoesNotContain(thirdMessage.Id, messages.Select(x => x.Id));
+ }
+
+ public void Dispose()
+ {
+ _outboxProvider.DeleteStore(_createdMessages);
+ }
+}
diff --git a/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Sync/When_Deleting_One_Message_It_Should_Be_Removed_From_Outbox.cs b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Sync/When_Deleting_One_Message_It_Should_Be_Removed_From_Outbox.cs
new file mode 100644
index 0000000000..6022a2ca70
--- /dev/null
+++ b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Sync/When_Deleting_One_Message_It_Should_Be_Removed_From_Outbox.cs
@@ -0,0 +1,84 @@
+#region Licence
+
+/* The MIT License (MIT)
+Copyright © 2014 Ian Cooper
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE. */
+
+#endregion
+
+//
+// This file is auto-generated by Paramore.Brighter.Test.Generator
+//
+
+using Xunit;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Paramore.Brighter.Oracle.Tests.Outbox.Binary.Sync;
+
+[Trait("Category", "Oracle")]
+public class WhenDeletingOneMessageItShouldBeRemovedFromOutbox : IDisposable
+{
+ private readonly IAmAnOutboxProviderSync _outboxProvider;
+ private List _createdMessages = [];
+
+ public WhenDeletingOneMessageItShouldBeRemovedFromOutbox()
+ {
+ _outboxProvider = new OracleBinaryOutboxProvider();
+ _outboxProvider.CreateStore();
+ }
+
+ [Fact]
+ public void When_Deleting_One_Message_It_Should_Be_Removed_From_Outbox()
+ {
+ // Arrange
+ var context = new RequestContext();
+ var firstMessage = new DefaultMessageBuilder().Build();
+ var secondMessage = new DefaultMessageBuilder().Build();
+ var thirdMessage = new DefaultMessageBuilder().Build();
+
+ _createdMessages.Add(firstMessage);
+ _createdMessages.Add(secondMessage);
+ _createdMessages.Add(thirdMessage);
+
+ // Act
+ var outbox = _outboxProvider.CreateOutbox();
+ outbox.Add(firstMessage, context);
+ outbox.Add(secondMessage, context);
+ outbox.Add(thirdMessage, context);
+
+ outbox.Delete([firstMessage.Id], context);
+
+ // Assert
+ var messages = outbox
+ .OutstandingMessages(TimeSpan.Zero, context)
+ .ToArray();
+
+ Assert.DoesNotContain(firstMessage.Id, messages.Select(x => x.Id));
+ Assert.Contains(secondMessage.Id, messages.Select(x => x.Id));
+ Assert.Contains(thirdMessage.Id, messages.Select(x => x.Id));
+ }
+
+ public void Dispose()
+ {
+ _outboxProvider.DeleteStore(_createdMessages);
+ }
+}
diff --git a/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Sync/When_Retrieving_A_Message_By_Id_It_Should_Return_The_Correct_Message.cs b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Sync/When_Retrieving_A_Message_By_Id_It_Should_Return_The_Correct_Message.cs
new file mode 100644
index 0000000000..ef0db87a87
--- /dev/null
+++ b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Sync/When_Retrieving_A_Message_By_Id_It_Should_Return_The_Correct_Message.cs
@@ -0,0 +1,109 @@
+#region Licence
+
+/* The MIT License (MIT)
+Copyright © 2014 Ian Cooper
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE. */
+
+#endregion
+
+//
+// This file is auto-generated by Paramore.Brighter.Test.Generator
+//
+
+using Xunit;
+using System;
+using System.Collections.Generic;
+
+namespace Paramore.Brighter.Oracle.Tests.Outbox.Binary.Sync;
+
+[Trait("Category", "Oracle")]
+public class WhenRetrievingAMessageByIdItShouldReturnTheCorrectMessage : IDisposable
+{
+ private readonly IAmAnOutboxProviderSync _outboxProvider;
+ private List _createdMessages = [];
+
+ public WhenRetrievingAMessageByIdItShouldReturnTheCorrectMessage()
+ {
+ _outboxProvider = new OracleBinaryOutboxProvider();
+ _outboxProvider.CreateStore();
+ }
+
+ [Fact]
+ public void When_Retrieving_A_Message_By_Id_It_Should_Return_The_Correct_Message()
+ {
+ // Arrange
+ var context = new RequestContext();
+ var earliest = new DefaultMessageBuilder().Build();
+ var dispatched = new DefaultMessageBuilder().Build();
+ var undispatched = new DefaultMessageBuilder().Build();
+
+ _createdMessages.Add(earliest);
+ _createdMessages.Add(dispatched);
+ _createdMessages.Add(undispatched);
+
+ var outbox = _outboxProvider.CreateOutbox();
+ outbox.Add([earliest, dispatched, undispatched], context);
+ outbox.MarkDispatched(earliest.Id, context, DateTime.UtcNow.AddHours(-3));
+ outbox.MarkDispatched(dispatched.Id, context, DateTime.UtcNow.AddSeconds(-30));
+
+ // Act
+ var message = outbox.Get(dispatched.Id, context);
+
+ // Assert
+ Assert.NotNull(message);
+ Assert.Equal(message.Body.Value, dispatched.Body.Value);
+
+ //should read the header from the sql outbox
+ Assert.Equal(message.Header.Topic, dispatched.Header.Topic);
+ Assert.Equal(message.Header.MessageType, dispatched.Header.MessageType);
+ Assert.Equal(message.Header.TimeStamp, dispatched.Header.TimeStamp, TimeSpan.FromSeconds(1));
+ Assert.Equal(0, dispatched.Header.HandledCount); // -- should be zero when read from outbox
+ // Assert.Equal(TimeSpan.Zero, dispatched.Header.Delayed); // -- should be zero when read from outbox
+ Assert.Equal(message.Header.CorrelationId, dispatched.Header.CorrelationId);
+ Assert.Equal(message.Header.ReplyTo, dispatched.Header.ReplyTo);
+ Assert.StartsWith(message.Header.ContentType.MediaType, dispatched.Header.ContentType.ToString());
+ Assert.Equal(message.Header.PartitionKey, dispatched.Header.PartitionKey);
+
+ //Bag serialization
+ Assert.Equal(message.Header.Bag.Count, dispatched.Header.Bag.Count);
+ foreach (var (key, val) in message.Header.Bag)
+ {
+ Assert.Contains(key, dispatched.Header.Bag);
+ Assert.Equal(val.ToString(), dispatched.Header.Bag[key].ToString());
+ }
+
+ //Asserts for workflow properties
+ Assert.Equal(message.Header.WorkflowId, dispatched.Header.WorkflowId);
+ Assert.Equal(message.Header.JobId, dispatched.Header.JobId);
+
+ // new fields assertions
+ Assert.Equal(message.Header.Source, dispatched.Header.Source);
+ Assert.Equal(message.Header.Type, dispatched.Header.Type);
+ Assert.Equal(message.Header.DataSchema, dispatched.Header.DataSchema);
+ Assert.Equal(message.Header.Subject, dispatched.Header.Subject);
+ Assert.Equal(message.Header.TraceParent, dispatched.Header.TraceParent);
+ Assert.Equal(message.Header.TraceState, dispatched.Header.TraceState);
+ }
+
+ public void Dispose()
+ {
+ _outboxProvider.DeleteStore(_createdMessages);
+ }
+}
diff --git a/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Sync/When_Retrieving_A_Non_Existent_Message_It_Should_Return_Empty_Message.cs b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Sync/When_Retrieving_A_Non_Existent_Message_It_Should_Return_Empty_Message.cs
new file mode 100644
index 0000000000..8c9f3a1177
--- /dev/null
+++ b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Sync/When_Retrieving_A_Non_Existent_Message_It_Should_Return_Empty_Message.cs
@@ -0,0 +1,70 @@
+#region Licence
+
+/* The MIT License (MIT)
+Copyright © 2014 Ian Cooper
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE. */
+
+#endregion
+
+//
+// This file is auto-generated by Paramore.Brighter.Test.Generator
+//
+
+using Xunit;
+using System;
+using System.Collections.Generic;
+
+namespace Paramore.Brighter.Oracle.Tests.Outbox.Binary.Sync;
+
+[Trait("Category", "Oracle")]
+public class WhenRetrievingANonExistentMessageItShouldReturnEmptyMessage : IDisposable
+{
+ private readonly IAmAnOutboxProviderSync _outboxProvider;
+ private readonly IAmAMessageBuilder _messageBuilder;
+ private List _createdMessages = [];
+
+ public WhenRetrievingANonExistentMessageItShouldReturnEmptyMessage()
+ {
+ _outboxProvider = new OracleBinaryOutboxProvider();
+ _outboxProvider.CreateStore();
+
+ _messageBuilder = new DefaultMessageBuilder();
+ }
+
+ [Fact]
+ public void When_Retrieving_A_Non_Existent_Message_It_Should_Return_Empty_Message()
+ {
+ // Arrange
+ var context = new RequestContext();
+
+ var outbox = _outboxProvider.CreateOutbox();
+
+ // Act
+ var message = outbox.Get(Id.Random(), context);
+
+ // Assert
+ Assert.Equal(MessageType.MT_NONE, message.Header.MessageType);
+ }
+
+ public void Dispose()
+ {
+ _outboxProvider.DeleteStore(_createdMessages);
+ }
+}
diff --git a/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Sync/When_Retrieving_All_Messages_They_Should_Include_Dispatched_And_Undispatched.cs b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Sync/When_Retrieving_All_Messages_They_Should_Include_Dispatched_And_Undispatched.cs
new file mode 100644
index 0000000000..f9c1a2d22e
--- /dev/null
+++ b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Sync/When_Retrieving_All_Messages_They_Should_Include_Dispatched_And_Undispatched.cs
@@ -0,0 +1,81 @@
+#region Licence
+
+/* The MIT License (MIT)
+Copyright © 2014 Ian Cooper
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE. */
+
+#endregion
+
+//
+// This file is auto-generated by Paramore.Brighter.Test.Generator
+//
+
+using Xunit;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Paramore.Brighter.Oracle.Tests.Outbox.Binary.Sync;
+
+[Trait("Category", "Oracle")]
+public class WhenRetrievingAllMessagesTheyShouldIncludeDispatchedAndUndispatched : IDisposable
+{
+ private readonly IAmAnOutboxProviderSync _outboxProvider;
+ private List _createdMessages = [];
+
+ public WhenRetrievingAllMessagesTheyShouldIncludeDispatchedAndUndispatched()
+ {
+ _outboxProvider = new OracleBinaryOutboxProvider();
+ _outboxProvider.CreateStore();
+ }
+
+ [Fact]
+ public void When_Retrieving_All_Messages_They_Should_Include_Dispatched_And_Undispatched()
+ {
+ // Arrange
+ var context = new RequestContext();
+ var earliest = new DefaultMessageBuilder().Build();
+ var dispatched = new DefaultMessageBuilder().Build();
+ var undispatched = new DefaultMessageBuilder().Build();
+
+ _createdMessages.Add(earliest);
+ _createdMessages.Add(dispatched);
+ _createdMessages.Add(undispatched);
+
+ var outbox = _outboxProvider.CreateOutbox();
+ outbox.Add([earliest, dispatched, undispatched], context);
+ outbox.MarkDispatched(earliest.Id, context, DateTime.UtcNow.AddHours(-3));
+ outbox.MarkDispatched(dispatched.Id, context, DateTime.UtcNow.AddSeconds(-30));
+
+ // Act
+ var messages = _outboxProvider.GetAllMessages().ToArray();
+
+ // Assert
+ Assert.True(messages.Length >= 3, "Expecting at least 3 messages");
+ Assert.Contains(earliest.Id, messages.Select(x => x.Id));
+ Assert.Contains(dispatched.Id, messages.Select(x => x.Id));
+ Assert.Contains(undispatched.Id, messages.Select(x => x.Id));
+ }
+
+ public void Dispose()
+ {
+ _outboxProvider.DeleteStore(_createdMessages);
+ }
+}
diff --git a/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Sync/When_Retrieving_Dispatched_Messages_It_Should_Filter_By_Age.cs b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Sync/When_Retrieving_Dispatched_Messages_It_Should_Filter_By_Age.cs
new file mode 100644
index 0000000000..da785f0881
--- /dev/null
+++ b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Sync/When_Retrieving_Dispatched_Messages_It_Should_Filter_By_Age.cs
@@ -0,0 +1,92 @@
+#region Licence
+
+/* The MIT License (MIT)
+Copyright © 2014 Ian Cooper
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE. */
+
+#endregion
+
+//
+// This file is auto-generated by Paramore.Brighter.Test.Generator
+//
+
+using Xunit;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Paramore.Brighter.Oracle.Tests.Outbox.Binary.Sync;
+
+[Trait("Category", "Oracle")]
+public class WhenRetrievingDispatchedMessagesItShouldFilterByAge : IDisposable
+{
+ private readonly IAmAnOutboxProviderSync _outboxProvider;
+ private List _createdMessages = [];
+
+ public WhenRetrievingDispatchedMessagesItShouldFilterByAge()
+ {
+ _outboxProvider = new OracleBinaryOutboxProvider();
+ _outboxProvider.CreateStore();
+ }
+
+ [Fact]
+ public void When_Retrieving_Dispatched_Messages_It_Should_Filter_By_Age()
+ {
+ // Arrange
+ var context = new RequestContext();
+ var earliest = new DefaultMessageBuilder().Build();
+ var dispatched = new DefaultMessageBuilder().Build();
+ var undispatched = new DefaultMessageBuilder().Build();
+
+ _createdMessages.Add(earliest);
+ _createdMessages.Add(dispatched);
+ _createdMessages.Add(undispatched);
+
+ var outbox = _outboxProvider.CreateOutbox();
+ outbox.Add([earliest, dispatched, undispatched], context);
+ outbox.MarkDispatched(earliest.Id, context, DateTime.UtcNow.AddHours(-3));
+ outbox.MarkDispatched(dispatched.Id, context, DateTime.UtcNow.AddSeconds(-30));
+
+ // Act
+ var allDispatched = outbox.DispatchedMessages(TimeSpan.Zero, context).ToArray();
+ var messagesOverAnHour = outbox.DispatchedMessages(TimeSpan.FromHours(1), context).ToArray();
+ var messagesOver4Hours = outbox.DispatchedMessages(TimeSpan.FromHours(4), context).ToArray();
+
+ // Assert
+ Assert.True(allDispatched.Length >= 2, "Expecting at least 2 messages");
+ Assert.Contains(earliest.Id, allDispatched.Select(x => x.Id));
+ Assert.Contains(dispatched.Id, allDispatched.Select(x => x.Id));
+ Assert.DoesNotContain(undispatched.Id, allDispatched.Select(x => x.Id));
+
+ Assert.True(messagesOverAnHour.Length >= 1, "Expecting at least 1 message");
+ Assert.Contains(earliest.Id, messagesOverAnHour.Select(x => x.Id));
+ Assert.DoesNotContain(dispatched.Id, messagesOverAnHour.Select(x => x.Id));
+ Assert.DoesNotContain(undispatched.Id, messagesOverAnHour.Select(x => x.Id));
+
+ Assert.DoesNotContain(earliest.Id, messagesOver4Hours.Select(x => x.Id));
+ Assert.DoesNotContain(dispatched.Id, messagesOver4Hours.Select(x => x.Id));
+ Assert.DoesNotContain(undispatched.Id, messagesOver4Hours.Select(x => x.Id));
+ }
+
+ public void Dispose()
+ {
+ _outboxProvider.DeleteStore(_createdMessages);
+ }
+}
diff --git a/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Sync/When_Retrieving_Messages_By_Ids_It_Should_Return_Only_Requested_Messages.cs b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Sync/When_Retrieving_Messages_By_Ids_It_Should_Return_Only_Requested_Messages.cs
new file mode 100644
index 0000000000..0856e67a5f
--- /dev/null
+++ b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Sync/When_Retrieving_Messages_By_Ids_It_Should_Return_Only_Requested_Messages.cs
@@ -0,0 +1,83 @@
+#region Licence
+
+/* The MIT License (MIT)
+Copyright © 2014 Ian Cooper
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE. */
+
+#endregion
+
+//
+// This file is auto-generated by Paramore.Brighter.Test.Generator
+//
+
+using Xunit;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Paramore.Brighter.Oracle.Tests.Outbox.Binary.Sync;
+
+[Trait("Category", "Oracle")]
+public class WhenRetrievingMessagesByIdsItShouldReturnOnlyRequestedMessages : IDisposable
+{
+ private readonly IAmAnOutboxProviderSync _outboxProvider;
+ private List _createdMessages = [];
+
+ public WhenRetrievingMessagesByIdsItShouldReturnOnlyRequestedMessages()
+ {
+ _outboxProvider = new OracleBinaryOutboxProvider();
+ _outboxProvider.CreateStore();
+ }
+
+ [Fact]
+ public void When_Retrieving_Messages_By_Ids_It_Should_Return_Only_Requested_Messages()
+ {
+ // Arrange
+ var context = new RequestContext();
+ var earliest = new DefaultMessageBuilder().Build();
+ var dispatched = new DefaultMessageBuilder().Build();
+ var undispatched = new DefaultMessageBuilder().Build();
+
+ _createdMessages.Add(earliest);
+ _createdMessages.Add(dispatched);
+ _createdMessages.Add(undispatched);
+
+ var outbox = _outboxProvider.CreateOutbox();
+ outbox.Add([earliest, dispatched, undispatched], context);
+ outbox.MarkDispatched(earliest.Id, context, DateTime.UtcNow.AddHours(-3));
+ outbox.MarkDispatched(dispatched.Id, context, DateTime.UtcNow.AddSeconds(-30));
+
+ // Act
+ var messages = outbox
+ .Get([earliest.Id, undispatched.Id], context)
+ .ToArray();
+
+ // Assert
+ Assert.Equal(2, messages.Length);
+ Assert.Contains(earliest.Id, messages.Select(x => x.Id));
+ Assert.DoesNotContain(dispatched.Id, messages.Select(x => x.Id));
+ Assert.Contains(undispatched.Id, messages.Select(x => x.Id));
+ }
+
+ public void Dispose()
+ {
+ _outboxProvider.DeleteStore(_createdMessages);
+ }
+}
diff --git a/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Sync/When_Retrieving_Outstanding_Messages_It_Should_Filter_By_Age.cs b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Sync/When_Retrieving_Outstanding_Messages_It_Should_Filter_By_Age.cs
new file mode 100644
index 0000000000..8646a44770
--- /dev/null
+++ b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Sync/When_Retrieving_Outstanding_Messages_It_Should_Filter_By_Age.cs
@@ -0,0 +1,92 @@
+#region Licence
+
+/* The MIT License (MIT)
+Copyright © 2014 Ian Cooper
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE. */
+
+#endregion
+
+//
+// This file is auto-generated by Paramore.Brighter.Test.Generator
+//
+
+using Xunit;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Paramore.Brighter.Oracle.Tests.Outbox.Binary.Sync;
+
+[Trait("Category", "Oracle")]
+public class WhenRetrievingOutstandingMessagesItShouldFilterByAge : IDisposable
+{
+ private readonly IAmAnOutboxProviderSync _outboxProvider;
+ private List _createdMessages = [];
+
+ public WhenRetrievingOutstandingMessagesItShouldFilterByAge()
+ {
+ _outboxProvider = new OracleBinaryOutboxProvider();
+ _outboxProvider.CreateStore();
+ }
+
+ [Fact]
+ public void When_Retrieving_Outstanding_Messages_It_Should_Filter_By_Age()
+ {
+ // Arrange
+ var context = new RequestContext();
+ var earliest = new DefaultMessageBuilder().Build();
+ var dispatched = new DefaultMessageBuilder().Build();
+ var undispatched = new DefaultMessageBuilder().Build();
+
+ _createdMessages.Add(earliest);
+ _createdMessages.Add(dispatched);
+ _createdMessages.Add(undispatched);
+
+ var outbox = _outboxProvider.CreateOutbox();
+ outbox.Add([earliest, dispatched, undispatched], context);
+ outbox.MarkDispatched(earliest.Id, context, DateTime.UtcNow.AddHours(-3));
+ outbox.MarkDispatched(dispatched.Id, context, DateTime.UtcNow.AddSeconds(-30));
+
+ // Act
+ var allDispatched = outbox.DispatchedMessages(TimeSpan.Zero, context).ToArray();
+ var messagesOverAnHour = outbox.DispatchedMessages(TimeSpan.FromHours(1), context).ToArray();
+ var messagesOver4Hours = outbox.DispatchedMessages(TimeSpan.FromHours(4), context).ToArray();
+
+ // Assert
+ Assert.True(allDispatched.Length >= 2, "Expecting at least 2 messages");
+ Assert.Contains(earliest.Id, allDispatched.Select(x => x.Id));
+ Assert.Contains(dispatched.Id, allDispatched.Select(x => x.Id));
+ Assert.DoesNotContain(undispatched.Id, allDispatched.Select(x => x.Id));
+
+ Assert.True(messagesOverAnHour.Length >= 1, "Expecting at least 1 message");
+ Assert.Contains(earliest.Id, messagesOverAnHour.Select(x => x.Id));
+ Assert.DoesNotContain(dispatched.Id, messagesOverAnHour.Select(x => x.Id));
+ Assert.DoesNotContain(undispatched.Id, messagesOverAnHour.Select(x => x.Id));
+
+ Assert.DoesNotContain(earliest.Id, messagesOver4Hours.Select(x => x.Id));
+ Assert.DoesNotContain(dispatched.Id, messagesOver4Hours.Select(x => x.Id));
+ Assert.DoesNotContain(undispatched.Id, messagesOver4Hours.Select(x => x.Id));
+ }
+
+ public void Dispose()
+ {
+ _outboxProvider.DeleteStore(_createdMessages);
+ }
+}
diff --git a/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/OracleBinaryOutboxProvider.cs b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/OracleBinaryOutboxProvider.cs
new file mode 100644
index 0000000000..0ba52d5849
--- /dev/null
+++ b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/OracleBinaryOutboxProvider.cs
@@ -0,0 +1,89 @@
+using System.Collections.Generic;
+using System.Data.Common;
+using System.Threading.Tasks;
+using Oracle.ManagedDataAccess.Client;
+using Paramore.Brighter.Oracle.Tests.Outbox.Binary.Async;
+using Paramore.Brighter.Oracle.Tests.Outbox.Binary.Sync;
+using Paramore.Brighter.Outbox.Oracle;
+
+namespace Paramore.Brighter.Oracle.Tests.Outbox.Binary;
+
+public class OracleBinaryOutboxProvider : IAmAnOutboxProviderSync, IAmAnOutboxProviderAsync
+{
+ private readonly RelationalDatabaseConfiguration _configuration = new(
+ Const.DefaultConnectingString,
+ outBoxTableName: $"{Const.TablePrefix}{Uuid.New():N}",
+ binaryMessagePayload: true);
+
+ private readonly OracleTransactionProvider _transactionProvider;
+ private readonly OracleOutbox _outbox;
+
+ public OracleBinaryOutboxProvider()
+ {
+ _transactionProvider = new OracleTransactionProvider(_configuration);
+ _outbox = new OracleOutbox(_configuration, _transactionProvider);
+ }
+
+ public IAmAnOutboxSync CreateOutbox()
+ {
+ return _outbox;
+ }
+
+ public IAmAnOutboxAsync CreateOutboxAsync()
+ {
+ return _outbox;
+ }
+
+ public void CreateStore()
+ {
+ using var connection = new OracleConnection(_configuration.ConnectionString);
+ connection.Open();
+ using var command = connection.CreateCommand();
+ command.CommandText = OracleOutboxBuilder.GetDDL(_configuration.OutBoxTableName, true);
+ command.ExecuteNonQuery();
+ }
+
+ public async Task CreateStoreAsync()
+ {
+ await using var connection = new OracleConnection(_configuration.ConnectionString);
+ await connection.OpenAsync();
+ await using var command = connection.CreateCommand();
+ command.CommandText = OracleOutboxBuilder.GetDDL(_configuration.OutBoxTableName, true);
+ await command.ExecuteNonQueryAsync();
+ }
+
+ public IAmABoxTransactionProvider CreateTransactionProvider()
+ {
+ return _transactionProvider;
+ }
+
+ public void DeleteStore(IEnumerable messages)
+ {
+ using var connection = new OracleConnection(_configuration.ConnectionString);
+ connection.Open();
+ using var command = connection.CreateCommand();
+ command.CommandText = $"DROP TABLE {_configuration.OutBoxTableName}";
+ command.ExecuteNonQuery();
+ }
+
+ public async Task DeleteStoreAsync(IEnumerable messages)
+ {
+ await using var connection = new OracleConnection(_configuration.ConnectionString);
+ await connection.OpenAsync();
+ await using var command = connection.CreateCommand();
+ command.CommandText = $"DROP TABLE {_configuration.OutBoxTableName}";
+ await command.ExecuteNonQueryAsync();
+ }
+
+ public IEnumerable GetAllMessages()
+ {
+ var outbox = new OracleOutbox(_configuration);
+ return outbox.Get(new RequestContext());
+ }
+
+ public async Task> GetAllMessagesAsync()
+ {
+ var outbox = new OracleOutbox(_configuration);
+ return await outbox.GetAsync(new RequestContext());
+ }
+}
diff --git a/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Async/IAmAnOutboxProviderAsync.cs b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Async/IAmAnOutboxProviderAsync.cs
new file mode 100644
index 0000000000..3a99c6cde3
--- /dev/null
+++ b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Async/IAmAnOutboxProviderAsync.cs
@@ -0,0 +1,68 @@
+#region Licence
+
+/* The MIT License (MIT)
+Copyright © 2014 Ian Cooper
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE. */
+
+#endregion
+
+//
+// This file is auto-generated by Paramore.Brighter.Test.Generator
+//
+
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+namespace Paramore.Brighter.Oracle.Tests.Outbox.Text.Async;
+
+///
+/// Provider for managing outbox storage and creating outbox instances.
+///
+public interface IAmAnOutboxProviderAsync
+{
+ ///
+ /// Creates the outbox data store.
+ ///
+ Task CreateStoreAsync();
+
+ ///
+ /// Deletes the outbox data store and removes the specified messages.
+ ///
+ /// The messages to remove from the store.
+ Task DeleteStoreAsync(IEnumerable messages);
+
+ ///
+ /// Creates a new outbox instance for storing and retrieving messages.
+ ///
+ /// A new instance.
+ IAmAnOutboxAsync CreateOutboxAsync();
+
+ ///
+ /// Gets all the messages in the outbox.
+ ///
+ /// A list of all messages in the outbox.
+ Task> GetAllMessagesAsync();
+
+ ///
+ /// Creates a new transaction provider.
+ ///
+ /// A new instance.
+ IAmABoxTransactionProvider CreateTransactionProvider();
+}
diff --git a/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Async/When_Adding_A_Duplicate_Message_It_Should_Not_Throw_Async.cs b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Async/When_Adding_A_Duplicate_Message_It_Should_Not_Throw_Async.cs
new file mode 100644
index 0000000000..db0d2f1dc4
--- /dev/null
+++ b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Async/When_Adding_A_Duplicate_Message_It_Should_Not_Throw_Async.cs
@@ -0,0 +1,54 @@
+//
+// This file is auto-generated by Paramore.Brighter.Test.Generator
+//
+
+using Xunit;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace Paramore.Brighter.Oracle.Tests.Outbox.Text.Async;
+
+[Trait("Category", "Oracle")]
+[Collection("OracleTextOutbox")]
+public class WhenAddingADuplicateMessageItShouldNotThrowAsync : IAsyncLifetime
+{
+ private readonly IAmAnOutboxProviderAsync _outboxProvider;
+ private readonly IAmAMessageBuilder _messageBuilder;
+ private List _createdMessages = [];
+
+ public WhenAddingADuplicateMessageItShouldNotThrowAsync()
+ {
+ _outboxProvider = new OracleTextOutboxProvider();
+ _messageBuilder = new DefaultMessageBuilder();
+ }
+
+ public async Task InitializeAsync()
+ {
+ await _outboxProvider.CreateStoreAsync();
+ }
+
+ public async Task DisposeAsync()
+ {
+ await _outboxProvider.DeleteStoreAsync(_createdMessages);
+ }
+
+ [Fact]
+ public async Task When_Adding_A_Duplicate_Message_It_Should_Not_Throw_Async()
+ {
+ // Arrange
+ var context = new RequestContext();
+ var message = _messageBuilder.Build();
+
+ _createdMessages.Add(message);
+
+ // Act
+ var outbox = _outboxProvider.CreateOutboxAsync();
+ await outbox.AddAsync(message, context);
+
+ // Assert
+ // Just adding a simple assertion to remove any warning
+ Assert.True(true);
+ }
+}
diff --git a/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Async/When_Adding_A_Message_It_Should_Be_Stored_With_All_Properties_Async.cs b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Async/When_Adding_A_Message_It_Should_Be_Stored_With_All_Properties_Async.cs
new file mode 100644
index 0000000000..cead1f2337
--- /dev/null
+++ b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Async/When_Adding_A_Message_It_Should_Be_Stored_With_All_Properties_Async.cs
@@ -0,0 +1,87 @@
+//
+// This file is auto-generated by Paramore.Brighter.Test.Generator
+//
+
+using Xunit;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace Paramore.Brighter.Oracle.Tests.Outbox.Text.Async;
+
+[Trait("Category", "Oracle")]
+[Collection("OracleTextOutbox")]
+public class WhenAddingAMessageItShouldBeStoredWithAllPropertiesAsync : IAsyncLifetime
+{
+ private readonly IAmAnOutboxProviderAsync _outboxProvider;
+ private readonly IAmAMessageBuilder _messageBuilder;
+ private List _createdMessages = [];
+
+ public WhenAddingAMessageItShouldBeStoredWithAllPropertiesAsync()
+ {
+ _outboxProvider = new OracleTextOutboxProvider();
+
+ _messageBuilder = new DefaultMessageBuilder();
+ }
+
+ public async Task InitializeAsync()
+ {
+ await _outboxProvider.CreateStoreAsync();
+ }
+
+ public async Task DisposeAsync()
+ {
+ await _outboxProvider.DeleteStoreAsync(_createdMessages);
+ }
+
+ [Fact]
+ public async Task When_Adding_A_Message_It_Should_Be_Stored_With_All_Properties_Async()
+ {
+ // Arrange
+ var context = new RequestContext();
+ var message = _messageBuilder.Build();
+
+ _createdMessages.Add(message);
+
+ // Act
+ var outbox = _outboxProvider.CreateOutboxAsync();
+ await outbox.AddAsync(message, context);
+
+ var storedMessage = await outbox.GetAsync(message.Id, context);
+
+ // Assert
+ Assert.Equal(message.Body.Value, storedMessage.Body.Value);
+
+ //should read the header from the sql outbox
+ Assert.Equal(message.Header.Topic, storedMessage.Header.Topic);
+ Assert.Equal(message.Header.MessageType, storedMessage.Header.MessageType);
+ Assert.Equal(message.Header.TimeStamp, storedMessage.Header.TimeStamp, TimeSpan.FromSeconds(1));
+ Assert.Equal(0, storedMessage.Header.HandledCount); // -- should be zero when read from outbox
+ Assert.Equal(TimeSpan.Zero, storedMessage.Header.Delayed); // -- should be zero when read from outbox
+ Assert.Equal(message.Header.CorrelationId, storedMessage.Header.CorrelationId);
+ Assert.Equal(message.Header.ReplyTo, storedMessage.Header.ReplyTo);
+ Assert.StartsWith(message.Header.ContentType.ToString(), storedMessage.Header.ContentType.ToString());
+ Assert.Equal(message.Header.PartitionKey, storedMessage.Header.PartitionKey);
+
+ //Bag serialization
+ Assert.Equal(message.Header.Bag.Count, storedMessage.Header.Bag.Count);
+ foreach (var (key, val) in message.Header.Bag)
+ {
+ Assert.Contains(key, storedMessage.Header.Bag);
+ Assert.Equal(val, storedMessage.Header.Bag[key].ToString());
+ }
+
+ //Asserts for workflow properties
+ Assert.Equal(message.Header.WorkflowId, storedMessage.Header.WorkflowId);
+ Assert.Equal(message.Header.JobId, storedMessage.Header.JobId);
+
+ // new fields assertions
+ Assert.Equal(message.Header.Source, storedMessage.Header.Source);
+ Assert.Equal(message.Header.Type, storedMessage.Header.Type);
+ Assert.Equal(message.Header.DataSchema, storedMessage.Header.DataSchema);
+ Assert.Equal(message.Header.Subject, storedMessage.Header.Subject);
+ Assert.Equal(message.Header.TraceParent, storedMessage.Header.TraceParent);
+ Assert.Equal(message.Header.TraceState, storedMessage.Header.TraceState);
+ }
+}
diff --git a/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Async/When_Adding_A_Message_Within_Transaction_And_Rollback_It_Should_Not_Be_Stored_Async.cs b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Async/When_Adding_A_Message_Within_Transaction_And_Rollback_It_Should_Not_Be_Stored_Async.cs
new file mode 100644
index 0000000000..283bb3faa4
--- /dev/null
+++ b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Async/When_Adding_A_Message_Within_Transaction_And_Rollback_It_Should_Not_Be_Stored_Async.cs
@@ -0,0 +1,61 @@
+//
+// This file is auto-generated by Paramore.Brighter.Test.Generator
+//
+
+using Xunit;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace Paramore.Brighter.Oracle.Tests.Outbox.Text.Async;
+
+[Trait("Category", "Oracle")]
+[Collection("OracleTextOutbox")]
+public class WhenAddingAMessageWithinTransactionAndRollbackItShouldNotBeStoredAsync : IAsyncLifetime
+{
+ private readonly IAmAnOutboxProviderAsync _outboxProvider;
+ private readonly IAmAMessageBuilder _messageBuilder;
+ private List _createdMessages = [];
+
+ public WhenAddingAMessageWithinTransactionAndRollbackItShouldNotBeStoredAsync()
+ {
+ _outboxProvider = new OracleTextOutboxProvider();
+
+ _messageBuilder = new DefaultMessageBuilder();
+ }
+
+ public async Task InitializeAsync()
+ {
+ await _outboxProvider.CreateStoreAsync();
+ }
+
+ public async Task DisposeAsync()
+ {
+ await _outboxProvider.DeleteStoreAsync(_createdMessages);
+ }
+
+ [Fact]
+ public async Task When_Adding_A_Message_Within_Transaction_And_Rollback_It_Should_Not_Be_Stored_Async()
+ {
+ // Arrange
+ var outbox = _outboxProvider.CreateOutboxAsync();
+
+ var transaction = _outboxProvider.CreateTransactionProvider();
+ _ = await transaction.GetTransactionAsync();
+
+ var context = new RequestContext();
+ var message = _messageBuilder.Build();
+
+ _createdMessages.Add(message);
+
+ // Act
+ await outbox.AddAsync(message, context, transactionProvider: transaction);
+ await transaction.RollbackAsync();
+
+ var storedMessage = await outbox.GetAsync(message.Id, context);
+
+ // Assert
+ Assert.Equal(MessageType.MT_NONE, storedMessage.Header.MessageType);
+ }
+}
diff --git a/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Async/When_Adding_A_Message_Within_Transaction_It_Should_Be_Stored_After_Commit_Async.cs b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Async/When_Adding_A_Message_Within_Transaction_It_Should_Be_Stored_After_Commit_Async.cs
new file mode 100644
index 0000000000..19e11b198f
--- /dev/null
+++ b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Async/When_Adding_A_Message_Within_Transaction_It_Should_Be_Stored_After_Commit_Async.cs
@@ -0,0 +1,92 @@
+//
+// This file is auto-generated by Paramore.Brighter.Test.Generator
+//
+
+using Xunit;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace Paramore.Brighter.Oracle.Tests.Outbox.Text.Async;
+
+[Trait("Category", "Oracle")]
+[Collection("OracleTextOutbox")]
+public class WhenAddingAMessageWithinTransactionItShouldBeStoredAfterCommitAsync : IAsyncLifetime
+{
+ private readonly IAmAnOutboxProviderAsync _outboxProvider;
+ private readonly IAmAMessageBuilder _messageBuilder;
+ private List _createdMessages = [];
+
+ public WhenAddingAMessageWithinTransactionItShouldBeStoredAfterCommitAsync()
+ {
+ _outboxProvider = new OracleTextOutboxProvider();
+
+ _messageBuilder = new DefaultMessageBuilder();
+ }
+
+ public async Task InitializeAsync()
+ {
+ await _outboxProvider.CreateStoreAsync();
+ }
+
+ public async Task DisposeAsync()
+ {
+ await _outboxProvider.DeleteStoreAsync(_createdMessages);
+ }
+
+ [Fact]
+ public async Task When_Adding_A_Message_Within_Transaction_It_Should_Be_Stored_After_Commit_Async()
+ {
+ // Arrange
+ var outbox = _outboxProvider.CreateOutboxAsync();
+
+ var transaction = _outboxProvider.CreateTransactionProvider();
+ _ = await transaction.GetTransactionAsync();
+
+ var context = new RequestContext();
+ var message = _messageBuilder.Build();
+
+ _createdMessages.Add(message );
+
+ // Act
+ await outbox.AddAsync(message, context, transactionProvider: transaction);
+ await transaction.CommitAsync();
+
+ var storedMessage = await outbox.GetAsync(message.Id, context);
+
+ // Assert
+ Assert.Equal(message.Body.Value, storedMessage.Body.Value);
+
+ //should read the header from the sql outbox
+ Assert.Equal(message.Header.Topic, storedMessage.Header.Topic);
+ Assert.Equal(message.Header.MessageType, storedMessage.Header.MessageType);
+ Assert.Equal(message.Header.TimeStamp, storedMessage.Header.TimeStamp, TimeSpan.FromSeconds(1));
+ Assert.Equal(0, storedMessage.Header.HandledCount); // -- should be zero when read from outbox
+ Assert.Equal(TimeSpan.Zero, storedMessage.Header.Delayed); // -- should be zero when read from outbox
+ Assert.Equal(message.Header.CorrelationId, storedMessage.Header.CorrelationId);
+ Assert.Equal(message.Header.ReplyTo, storedMessage.Header.ReplyTo);
+ Assert.StartsWith(message.Header.ContentType.ToString(), storedMessage.Header.ContentType.ToString());
+ Assert.Equal(message.Header.PartitionKey, storedMessage.Header.PartitionKey);
+
+ //Bag serialization
+ Assert.Equal(message.Header.Bag.Count, storedMessage.Header.Bag.Count);
+ foreach (var (key, val) in message.Header.Bag)
+ {
+ Assert.Contains(key, storedMessage.Header.Bag);
+ Assert.Equal(val, storedMessage.Header.Bag[key].ToString());
+ }
+
+ //Asserts for workflow properties
+ Assert.Equal(message.Header.WorkflowId, storedMessage.Header.WorkflowId);
+ Assert.Equal(message.Header.JobId, storedMessage.Header.JobId);
+
+ // new fields assertions
+ Assert.Equal(message.Header.Source, storedMessage.Header.Source);
+ Assert.Equal(message.Header.Type, storedMessage.Header.Type);
+ Assert.Equal(message.Header.DataSchema, storedMessage.Header.DataSchema);
+ Assert.Equal(message.Header.Subject, storedMessage.Header.Subject);
+ Assert.Equal(message.Header.TraceParent, storedMessage.Header.TraceParent);
+ Assert.Equal(message.Header.TraceState, storedMessage.Header.TraceState);
+ }
+}
diff --git a/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Async/When_Deleting_Multiple_Messages_They_Should_Be_Removed_From_Outbox_Async.cs b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Async/When_Deleting_Multiple_Messages_They_Should_Be_Removed_From_Outbox_Async.cs
new file mode 100644
index 0000000000..1e8b7413eb
--- /dev/null
+++ b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Async/When_Deleting_Multiple_Messages_They_Should_Be_Removed_From_Outbox_Async.cs
@@ -0,0 +1,65 @@
+//
+// This file is auto-generated by Paramore.Brighter.Test.Generator
+//
+
+using Xunit;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace Paramore.Brighter.Oracle.Tests.Outbox.Text.Async;
+
+[Trait("Category", "Oracle")]
+[Collection("OracleTextOutbox")]
+public class WhenDeletingMultipleMessagesTheyShouldBeRemovedFromOutboxAsync : IAsyncLifetime
+{
+ private readonly IAmAnOutboxProviderAsync _outboxProvider;
+ private List _createdMessages = [];
+
+ public WhenDeletingMultipleMessagesTheyShouldBeRemovedFromOutboxAsync()
+ {
+ _outboxProvider = new OracleTextOutboxProvider();
+ }
+
+ public async Task InitializeAsync()
+ {
+ await _outboxProvider.CreateStoreAsync();
+ }
+
+ public async Task DisposeAsync()
+ {
+ await _outboxProvider.DeleteStoreAsync(_createdMessages);
+ }
+
+ [Fact]
+ public async Task When_Deleting_Multiple_Messages_They_Should_Be_Removed_From_Outbox_Async()
+ {
+ // Arrange
+ var context = new RequestContext();
+ var firstMessage = new DefaultMessageBuilder().Build();
+ var secondMessage = new DefaultMessageBuilder().Build();
+ var thirdMessage = new DefaultMessageBuilder().Build();
+
+ _createdMessages.Add(firstMessage);
+ _createdMessages.Add(secondMessage);
+ _createdMessages.Add(thirdMessage);
+
+ // Act
+ var outbox = _outboxProvider.CreateOutboxAsync();
+ await outbox.AddAsync(firstMessage, context);
+ await outbox.AddAsync(secondMessage, context);
+ await outbox.AddAsync(thirdMessage, context);
+
+ await outbox.DeleteAsync([firstMessage.Id, secondMessage.Id, thirdMessage.Id], context);
+
+ // Assert
+ var messages = (await outbox
+ .OutstandingMessagesAsync(TimeSpan.Zero, context))
+ .ToArray();
+
+ Assert.DoesNotContain(firstMessage.Id, messages.Select(x => x.Id));
+ Assert.DoesNotContain(secondMessage.Id, messages.Select(x => x.Id));
+ Assert.DoesNotContain(thirdMessage.Id, messages.Select(x => x.Id));
+ }
+}
diff --git a/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Async/When_Deleting_One_Message_It_Should_Be_Removed_From_Outbox_Async.cs b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Async/When_Deleting_One_Message_It_Should_Be_Removed_From_Outbox_Async.cs
new file mode 100644
index 0000000000..480fa6d7bc
--- /dev/null
+++ b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Async/When_Deleting_One_Message_It_Should_Be_Removed_From_Outbox_Async.cs
@@ -0,0 +1,91 @@
+#region Licence
+
+/* The MIT License (MIT)
+Copyright © 2014 Ian Cooper
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE. */
+
+#endregion
+
+//
+// This file is auto-generated by Paramore.Brighter.Test.Generator
+//
+
+using Xunit;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace Paramore.Brighter.Oracle.Tests.Outbox.Text.Async;
+
+[Trait("Category", "Oracle")]
+[Collection("OracleTextOutbox")]
+public class WhenDeletingOneMessageItShouldBeRemovedFromOutboxAsync : IAsyncLifetime
+{
+ private readonly IAmAnOutboxProviderAsync _outboxProvider;
+ private List _createdMessages = [];
+
+ public WhenDeletingOneMessageItShouldBeRemovedFromOutboxAsync()
+ {
+ _outboxProvider = new OracleTextOutboxProvider();
+ }
+
+ public async Task InitializeAsync()
+ {
+ await _outboxProvider.CreateStoreAsync();
+ }
+
+ public async Task DisposeAsync()
+ {
+ await _outboxProvider.DeleteStoreAsync(_createdMessages);
+ }
+
+ [Fact]
+ public async Task When_Deleting_One_Message_It_Should_Be_Removed_From_Outbox_Async()
+ {
+ // Arrange
+ var context = new RequestContext();
+
+ var firstMessage = new DefaultMessageBuilder().Build();
+ var secondMessage = new DefaultMessageBuilder().Build();
+ var thirdMessage = new DefaultMessageBuilder().Build();
+
+ _createdMessages.Add(firstMessage);
+ _createdMessages.Add(secondMessage);
+ _createdMessages.Add(thirdMessage);
+
+ // Act
+ var outbox = _outboxProvider.CreateOutboxAsync();
+ await outbox.AddAsync(firstMessage, context);
+ await outbox.AddAsync(secondMessage, context);
+ await outbox.AddAsync(thirdMessage, context);
+
+ await outbox.DeleteAsync([firstMessage.Id], context);
+
+ // Assert
+ var messages = (await outbox
+ .OutstandingMessagesAsync(TimeSpan.Zero, context))
+ .ToArray();
+
+ Assert.DoesNotContain(firstMessage.Id, messages.Select(x => x.Id));
+ Assert.Contains(secondMessage.Id, messages.Select(x => x.Id));
+ Assert.Contains(thirdMessage.Id, messages.Select(x => x.Id));
+ }
+}
diff --git a/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Async/When_Retrieving_A_Message_By_Id_It_Should_Return_The_Correct_Message_Async.cs b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Async/When_Retrieving_A_Message_By_Id_It_Should_Return_The_Correct_Message_Async.cs
new file mode 100644
index 0000000000..563aaaccdc
--- /dev/null
+++ b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Async/When_Retrieving_A_Message_By_Id_It_Should_Return_The_Correct_Message_Async.cs
@@ -0,0 +1,115 @@
+#region Licence
+
+/* The MIT License (MIT)
+Copyright © 2014 Ian Cooper
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE. */
+
+#endregion
+
+//
+// This file is auto-generated by Paramore.Brighter.Test.Generator
+//
+
+using Xunit;
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+namespace Paramore.Brighter.Oracle.Tests.Outbox.Text.Async;
+
+[Trait("Category", "Oracle")]
+[Collection("OracleTextOutbox")]
+public class WhenRetrievingAMessageByIdItShouldReturnTheCorrectMessageAsync : IAsyncLifetime
+{
+ private readonly IAmAnOutboxProviderAsync _outboxProvider;
+ private List _createdMessages = [];
+
+ public WhenRetrievingAMessageByIdItShouldReturnTheCorrectMessageAsync()
+ {
+ _outboxProvider = new OracleTextOutboxProvider();
+ }
+
+ public async Task InitializeAsync()
+ {
+ await _outboxProvider.CreateStoreAsync();
+ }
+
+ public async Task DisposeAsync()
+ {
+ await _outboxProvider.DeleteStoreAsync(_createdMessages);
+ }
+
+ [Fact]
+ public async Task When_Retrieving_A_Message_By_Id_It_Should_Return_The_Correct_Message_Async()
+ {
+ // Arrange
+ var context = new RequestContext();
+ var earliest = new DefaultMessageBuilder().Build();
+ var dispatched = new DefaultMessageBuilder().Build();
+ var undispatched = new DefaultMessageBuilder().Build();
+
+ _createdMessages.Add(earliest);
+ _createdMessages.Add(dispatched);
+ _createdMessages.Add(undispatched);
+
+ var outbox = _outboxProvider.CreateOutboxAsync();
+ await outbox.AddAsync([earliest, dispatched, undispatched], context);
+ await outbox.MarkDispatchedAsync(earliest.Id, context, DateTime.UtcNow.AddHours(-3));
+ await outbox.MarkDispatchedAsync(dispatched.Id, context, DateTime.UtcNow.AddSeconds(-30));
+
+ // Act
+ var message = await outbox.GetAsync(dispatched.Id, context);
+
+ // Assert
+ Assert.NotNull(message);
+ Assert.Equal(message.Body.Value, dispatched.Body.Value);
+
+ //should read the header from the sql outbox
+ Assert.Equal(message.Header.Topic, dispatched.Header.Topic);
+ Assert.Equal(message.Header.MessageType, dispatched.Header.MessageType);
+ Assert.Equal(message.Header.TimeStamp, dispatched.Header.TimeStamp, TimeSpan.FromSeconds(1));
+ Assert.Equal(0, dispatched.Header.HandledCount); // -- should be zero when read from outbox
+ // Assert.Equal(TimeSpan.Zero, dispatched.Header.Delayed); // -- should be zero when read from outbox
+ Assert.Equal(message.Header.CorrelationId, dispatched.Header.CorrelationId);
+ Assert.Equal(message.Header.ReplyTo, dispatched.Header.ReplyTo);
+ Assert.StartsWith(message.Header.ContentType.MediaType, dispatched.Header.ContentType.ToString());
+ Assert.Equal(message.Header.PartitionKey, dispatched.Header.PartitionKey);
+
+ //Bag serialization
+ Assert.Equal(message.Header.Bag.Count, dispatched.Header.Bag.Count);
+ foreach (var (key, val) in message.Header.Bag)
+ {
+ Assert.Contains(key, dispatched.Header.Bag);
+ Assert.Equal(val.ToString(), dispatched.Header.Bag[key].ToString());
+ }
+
+ //Asserts for workflow properties
+ Assert.Equal(message.Header.WorkflowId, dispatched.Header.WorkflowId);
+ Assert.Equal(message.Header.JobId, dispatched.Header.JobId);
+
+ // new fields assertions
+ Assert.Equal(message.Header.Source, dispatched.Header.Source);
+ Assert.Equal(message.Header.Type, dispatched.Header.Type);
+ Assert.Equal(message.Header.DataSchema, dispatched.Header.DataSchema);
+ Assert.Equal(message.Header.Subject, dispatched.Header.Subject);
+ Assert.Equal(message.Header.TraceParent, dispatched.Header.TraceParent);
+ Assert.Equal(message.Header.TraceState, dispatched.Header.TraceState);
+ }
+}
diff --git a/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Async/When_Retrieving_A_Non_Existent_Message_It_Should_Return_Empty_Message_Async.cs b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Async/When_Retrieving_A_Non_Existent_Message_It_Should_Return_Empty_Message_Async.cs
new file mode 100644
index 0000000000..b70032192e
--- /dev/null
+++ b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Async/When_Retrieving_A_Non_Existent_Message_It_Should_Return_Empty_Message_Async.cs
@@ -0,0 +1,52 @@
+//
+// This file is auto-generated by Paramore.Brighter.Test.Generator
+//
+
+using Xunit;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace Paramore.Brighter.Oracle.Tests.Outbox.Text.Async;
+
+[Trait("Category", "Oracle")]
+[Collection("OracleTextOutbox")]
+public class WhenRetrievingANonExistentMessageItShouldReturnEmptyMessageAsync : IAsyncLifetime
+{
+ private readonly IAmAnOutboxProviderAsync _outboxProvider;
+ private readonly IAmAMessageBuilder _messageBuilder;
+ private List _createdMessages = [];
+
+ public WhenRetrievingANonExistentMessageItShouldReturnEmptyMessageAsync()
+ {
+ _outboxProvider = new OracleTextOutboxProvider();
+
+ _messageBuilder = new DefaultMessageBuilder();
+ }
+
+ public async Task InitializeAsync()
+ {
+ await _outboxProvider.CreateStoreAsync();
+ }
+
+ public async Task DisposeAsync()
+ {
+ await _outboxProvider.DeleteStoreAsync(_createdMessages);
+ }
+
+ [Fact]
+ public async Task When_Retrieving_A_Non_Existent_Message_It_Should_Return_Empty_Message_Async()
+ {
+ // Arrange
+ var context = new RequestContext();
+
+ var outbox = _outboxProvider.CreateOutboxAsync();
+
+ // Act
+ var message = await outbox.GetAsync(Id.Random(), context);
+
+ // Assert
+ Assert.Equal(MessageType.MT_NONE, message.Header.MessageType);
+ }
+}
diff --git a/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Async/When_Retrieving_All_Messages_They_Should_Include_Dispatched_And_Undispatched_Async.cs b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Async/When_Retrieving_All_Messages_They_Should_Include_Dispatched_And_Undispatched_Async.cs
new file mode 100644
index 0000000000..b893c8a4de
--- /dev/null
+++ b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Async/When_Retrieving_All_Messages_They_Should_Include_Dispatched_And_Undispatched_Async.cs
@@ -0,0 +1,87 @@
+#region Licence
+
+/* The MIT License (MIT)
+Copyright © 2014 Ian Cooper
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE. */
+
+#endregion
+
+//
+// This file is auto-generated by Paramore.Brighter.Test.Generator
+//
+
+using Xunit;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace Paramore.Brighter.Oracle.Tests.Outbox.Text.Async;
+
+[Trait("Category", "Oracle")]
+[Collection("OracleTextOutbox")]
+public class WhenRetrievingAllMessagesTheyShouldIncludeDispatchedAndUndispatchedAsync : IAsyncLifetime
+{
+ private readonly IAmAnOutboxProviderAsync _outboxProvider;
+ private List _createdMessages = [];
+
+ public WhenRetrievingAllMessagesTheyShouldIncludeDispatchedAndUndispatchedAsync()
+ {
+ _outboxProvider = new OracleTextOutboxProvider();
+ }
+
+ public async Task InitializeAsync()
+ {
+ await _outboxProvider.CreateStoreAsync();
+ }
+
+ public async Task DisposeAsync()
+ {
+ await _outboxProvider.DeleteStoreAsync(_createdMessages);
+ }
+
+ [Fact]
+ public async Task When_Retrieving_All_Messages_They_Should_Include_Dispatched_And_Undispatched_Async()
+ {
+ // Arrange
+ var context = new RequestContext();
+ var earliest = new DefaultMessageBuilder().Build();
+ var dispatched = new DefaultMessageBuilder().Build();
+ var undispatched = new DefaultMessageBuilder().Build();
+
+ _createdMessages.Add(earliest);
+ _createdMessages.Add(dispatched);
+ _createdMessages.Add(undispatched);
+
+ var outbox = _outboxProvider.CreateOutboxAsync();
+ await outbox.AddAsync([earliest, dispatched, undispatched], context);
+ await outbox.MarkDispatchedAsync(earliest.Id, context, DateTime.UtcNow.AddHours(-3));
+ await outbox.MarkDispatchedAsync(dispatched.Id, context, DateTime.UtcNow.AddSeconds(-30));
+
+ // Act
+ var messages = (await _outboxProvider.GetAllMessagesAsync()).ToArray();
+
+ // Assert
+ Assert.True(messages.Length >= 3, "Expecting at least 3 messages");
+ Assert.Contains(earliest.Id, messages.Select(x => x.Id));
+ Assert.Contains(dispatched.Id, messages.Select(x => x.Id));
+ Assert.Contains(undispatched.Id, messages.Select(x => x.Id));
+ }
+}
diff --git a/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Async/When_Retrieving_Dispatched_Messages_It_Should_Filter_By_Age_Async.cs b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Async/When_Retrieving_Dispatched_Messages_It_Should_Filter_By_Age_Async.cs
new file mode 100644
index 0000000000..1217d34784
--- /dev/null
+++ b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Async/When_Retrieving_Dispatched_Messages_It_Should_Filter_By_Age_Async.cs
@@ -0,0 +1,73 @@
+//
+// This file is auto-generated by Paramore.Brighter.Test.Generator
+//
+
+using Xunit;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace Paramore.Brighter.Oracle.Tests.Outbox.Text.Async;
+
+[Trait("Category", "Oracle")]
+[Collection("OracleTextOutbox")]
+public class WhenRetrievingDispatchedMessagesItShouldFilterByAgeAsync : IAsyncLifetime
+{
+ private readonly IAmAnOutboxProviderAsync _outboxProvider;
+ private List _createdMessages = [];
+
+ public WhenRetrievingDispatchedMessagesItShouldFilterByAgeAsync()
+ {
+ _outboxProvider = new OracleTextOutboxProvider();
+ }
+
+ public async Task InitializeAsync()
+ {
+ await _outboxProvider.CreateStoreAsync();
+ }
+
+ public async Task DisposeAsync()
+ {
+ await _outboxProvider.DeleteStoreAsync(_createdMessages);
+ }
+
+ [Fact]
+ public async Task When_Retrieving_Dispatched_Messages_It_Should_Filter_By_Age_Async()
+ {
+ // Arrange
+ var context = new RequestContext();
+ var earliest = new DefaultMessageBuilder().Build();
+ var dispatched = new DefaultMessageBuilder().Build();
+ var undispatched = new DefaultMessageBuilder().Build();
+
+ _createdMessages.Add(earliest);
+ _createdMessages.Add(dispatched);
+ _createdMessages.Add(undispatched);
+
+ var outbox = _outboxProvider.CreateOutboxAsync();
+ await outbox.AddAsync([earliest, dispatched, undispatched], context);
+ await outbox.MarkDispatchedAsync(earliest.Id, context, DateTime.UtcNow.AddHours(-3));
+ await outbox.MarkDispatchedAsync(dispatched.Id, context, DateTime.UtcNow.AddSeconds(-30));
+
+ // Act
+ var allDispatched = (await outbox.DispatchedMessagesAsync(TimeSpan.Zero, context)).ToArray();
+ var messagesOverAnHour = (await outbox.DispatchedMessagesAsync(TimeSpan.FromHours(1), context)).ToArray();
+ var messagesOver4Hours = (await outbox.DispatchedMessagesAsync(TimeSpan.FromHours(4), context)).ToArray();
+
+ // Assert
+ Assert.True(allDispatched.Length >= 2, "Expecting at least 2 messages");
+ Assert.Contains(earliest.Id, allDispatched.Select(x => x.Id));
+ Assert.Contains(dispatched.Id, allDispatched.Select(x => x.Id));
+ Assert.DoesNotContain(undispatched.Id, allDispatched.Select(x => x.Id));
+
+ Assert.True(messagesOverAnHour.Length >= 1, "Expecting at least 1 message");
+ Assert.Contains(earliest.Id, messagesOverAnHour.Select(x => x.Id));
+ Assert.DoesNotContain(dispatched.Id, messagesOverAnHour.Select(x => x.Id));
+ Assert.DoesNotContain(undispatched.Id, messagesOverAnHour.Select(x => x.Id));
+
+ Assert.DoesNotContain(earliest.Id, messagesOver4Hours.Select(x => x.Id));
+ Assert.DoesNotContain(dispatched.Id, messagesOver4Hours.Select(x => x.Id));
+ Assert.DoesNotContain(undispatched.Id, messagesOver4Hours.Select(x => x.Id));
+ }
+}
diff --git a/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Async/When_Retrieving_Messages_By_Ids_It_Should_Return_Only_Requested_Messages_Async.cs b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Async/When_Retrieving_Messages_By_Ids_It_Should_Return_Only_Requested_Messages_Async.cs
new file mode 100644
index 0000000000..d9ea8f4bda
--- /dev/null
+++ b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Async/When_Retrieving_Messages_By_Ids_It_Should_Return_Only_Requested_Messages_Async.cs
@@ -0,0 +1,89 @@
+#region Licence
+
+/* The MIT License (MIT)
+Copyright © 2014 Ian Cooper
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE. */
+
+#endregion
+
+//
+// This file is auto-generated by Paramore.Brighter.Test.Generator
+//
+
+using Xunit;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace Paramore.Brighter.Oracle.Tests.Outbox.Text.Async;
+
+[Trait("Category", "Oracle")]
+[Collection("OracleTextOutbox")]
+public class WhenRetrievingMessagesByIdsItShouldReturnOnlyRequestedMessagesAsync : IAsyncLifetime
+{
+ private readonly IAmAnOutboxProviderAsync _outboxProvider;
+ private List _createdMessages = [];
+
+ public WhenRetrievingMessagesByIdsItShouldReturnOnlyRequestedMessagesAsync()
+ {
+ _outboxProvider = new OracleTextOutboxProvider();
+ }
+
+ public async Task InitializeAsync()
+ {
+ await _outboxProvider.CreateStoreAsync();
+ }
+
+ public async Task DisposeAsync()
+ {
+ await _outboxProvider.DeleteStoreAsync(_createdMessages);
+ }
+
+ [Fact]
+ public async Task When_Retrieving_Messages_By_Ids_It_Should_Return_Only_Requested_Messages_Async()
+ {
+ // Arrange
+ var context = new RequestContext();
+ var earliest = new DefaultMessageBuilder().Build();
+ var dispatched = new DefaultMessageBuilder().Build();
+ var undispatched = new DefaultMessageBuilder().Build();
+
+ _createdMessages.Add(earliest);
+ _createdMessages.Add(dispatched);
+ _createdMessages.Add(undispatched);
+
+ var outbox = _outboxProvider.CreateOutboxAsync();
+ await outbox.AddAsync([earliest, dispatched, undispatched], context);
+ await outbox.MarkDispatchedAsync(earliest.Id, context, DateTime.UtcNow.AddHours(-3));
+ await outbox.MarkDispatchedAsync(dispatched.Id, context, DateTime.UtcNow.AddSeconds(-30));
+
+ // Act
+ var messages = (await outbox
+ .GetAsync([earliest.Id, undispatched.Id], context))
+ .ToArray();
+
+ // Assert
+ Assert.Equal(2, messages.Length);
+ Assert.Contains(earliest.Id, messages.Select(x => x.Id));
+ Assert.DoesNotContain(dispatched.Id, messages.Select(x => x.Id));
+ Assert.Contains(undispatched.Id, messages.Select(x => x.Id));
+ }
+}
diff --git a/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Async/When_Retrieving_Outstanding_Messages_It_Should_Filter_By_Age_Async.cs b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Async/When_Retrieving_Outstanding_Messages_It_Should_Filter_By_Age_Async.cs
new file mode 100644
index 0000000000..4f946a2d7f
--- /dev/null
+++ b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Async/When_Retrieving_Outstanding_Messages_It_Should_Filter_By_Age_Async.cs
@@ -0,0 +1,73 @@
+//
+// This file is auto-generated by Paramore.Brighter.Test.Generator
+//
+
+using Xunit;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace Paramore.Brighter.Oracle.Tests.Outbox.Text.Async;
+
+[Trait("Category", "Oracle")]
+[Collection("OracleTextOutbox")]
+public class WhenRetrievingOutstandingMessagesItShouldFilterByAgeAsync : IAsyncLifetime
+{
+ private readonly IAmAnOutboxProviderAsync _outboxProvider;
+ private List _createdMessages = [];
+
+ public WhenRetrievingOutstandingMessagesItShouldFilterByAgeAsync()
+ {
+ _outboxProvider = new OracleTextOutboxProvider();
+ }
+
+ public async Task InitializeAsync()
+ {
+ await _outboxProvider.CreateStoreAsync();
+ }
+
+ public async Task DisposeAsync()
+ {
+ await _outboxProvider.DeleteStoreAsync(_createdMessages);
+ }
+
+ [Fact]
+ public async Task When_Retrieving_Outstanding_Messages_It_Should_Filter_By_Age_Async()
+ {
+ // Arrange
+ var context = new RequestContext();
+ var earliest = new DefaultMessageBuilder().Build();
+ var dispatched = new DefaultMessageBuilder().Build();
+ var undispatched = new DefaultMessageBuilder().Build();
+
+ _createdMessages.Add(earliest);
+ _createdMessages.Add(dispatched);
+ _createdMessages.Add(undispatched);
+
+ var outbox = _outboxProvider.CreateOutboxAsync();
+ await outbox.AddAsync([earliest, dispatched, undispatched], context);
+ await outbox.MarkDispatchedAsync(earliest.Id, context, DateTime.UtcNow.AddHours(-3));
+ await outbox.MarkDispatchedAsync(dispatched.Id, context, DateTime.UtcNow.AddSeconds(-30));
+
+ // Act
+ var allDispatched = (await outbox.DispatchedMessagesAsync(TimeSpan.Zero, context)).ToArray();
+ var messagesOverAnHour = (await outbox.DispatchedMessagesAsync(TimeSpan.FromHours(1), context)).ToArray();
+ var messagesOver4Hours = (await outbox.DispatchedMessagesAsync(TimeSpan.FromHours(4), context)).ToArray();
+
+ // Assert
+ Assert.True(allDispatched.Length >= 2, "Expecting at least 2 messages");
+ Assert.Contains(earliest.Id, allDispatched.Select(x => x.Id));
+ Assert.Contains(dispatched.Id, allDispatched.Select(x => x.Id));
+ Assert.DoesNotContain(undispatched.Id, allDispatched.Select(x => x.Id));
+
+ Assert.True(messagesOverAnHour.Length >= 1, "Expecting at least 1 message");
+ Assert.Contains(earliest.Id, messagesOverAnHour.Select(x => x.Id));
+ Assert.DoesNotContain(dispatched.Id, messagesOverAnHour.Select(x => x.Id));
+ Assert.DoesNotContain(undispatched.Id, messagesOverAnHour.Select(x => x.Id));
+
+ Assert.DoesNotContain(earliest.Id, messagesOver4Hours.Select(x => x.Id));
+ Assert.DoesNotContain(dispatched.Id, messagesOver4Hours.Select(x => x.Id));
+ Assert.DoesNotContain(undispatched.Id, messagesOver4Hours.Select(x => x.Id));
+ }
+}
diff --git a/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Sync/IAmAnOutboxProviderSync.cs b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Sync/IAmAnOutboxProviderSync.cs
new file mode 100644
index 0000000000..6e6596ffc9
--- /dev/null
+++ b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Sync/IAmAnOutboxProviderSync.cs
@@ -0,0 +1,67 @@
+#region Licence
+
+/* The MIT License (MIT)
+Copyright © 2014 Ian Cooper
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE. */
+
+#endregion
+
+//
+// This file is auto-generated by Paramore.Brighter.Test.Generator
+//
+
+using System.Collections.Generic;
+
+namespace Paramore.Brighter.Oracle.Tests.Outbox.Text.Sync;
+
+///
+/// Provider for managing outbox storage and creating outbox instances.
+///
+public interface IAmAnOutboxProviderSync
+{
+ ///
+ /// Creates the outbox data store.
+ ///
+ void CreateStore();
+
+ ///
+ /// Deletes the outbox data store and removes the specified messages.
+ ///
+ /// The messages to remove from the store.
+ void DeleteStore(IEnumerable messages);
+
+ ///
+ /// Creates a new outbox instance for storing and retrieving messages.
+ ///
+ /// A new instance.
+ IAmAnOutboxSync CreateOutbox();
+
+ ///
+ /// Gets all the messages in the outbox.
+ ///
+ /// A list of all messages in the outbox.
+ IEnumerable GetAllMessages();
+
+ ///
+ /// Creates a new transaction provider.
+ ///
+ /// A new instance.
+ IAmABoxTransactionProvider CreateTransactionProvider();
+}
diff --git a/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Sync/When_Adding_A_Duplicate_Message_It_Should_Not_Throw.cs b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Sync/When_Adding_A_Duplicate_Message_It_Should_Not_Throw.cs
new file mode 100644
index 0000000000..f8f02654f8
--- /dev/null
+++ b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Sync/When_Adding_A_Duplicate_Message_It_Should_Not_Throw.cs
@@ -0,0 +1,73 @@
+#region Licence
+
+/* The MIT License (MIT)
+Copyright © 2014 Ian Cooper
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE. */
+
+#endregion
+
+//
+// This file is auto-generated by Paramore.Brighter.Test.Generator
+//
+
+using Xunit;
+using System;
+using System.Collections.Generic;
+
+namespace Paramore.Brighter.Oracle.Tests.Outbox.Text.Sync;
+
+[Trait("Category", "Oracle")]
+public class WhenAddingADuplicateMessageItShouldNotThrow : IDisposable
+{
+ private readonly IAmAnOutboxProviderSync _outboxProvider;
+ private readonly IAmAMessageBuilder _messageBuilder;
+ private List _createdMessages = [];
+
+ public WhenAddingADuplicateMessageItShouldNotThrow()
+ {
+ _outboxProvider = new OracleTextOutboxProvider();
+ _outboxProvider.CreateStore();
+
+ _messageBuilder = new DefaultMessageBuilder();
+ }
+
+ [Fact]
+ public void When_Adding_A_Duplicate_Message_It_Should_Not_Throw()
+ {
+ // Arrange
+ var context = new RequestContext();
+ var message = _messageBuilder.Build();
+
+ _createdMessages.Add(message);
+
+ // Act
+ var outbox = _outboxProvider.CreateOutbox();
+ outbox.Add(message, context);
+
+ // Assert
+ // Just adding a simple assertion to remove any warning
+ Assert.True(true);
+ }
+
+ public void Dispose()
+ {
+ _outboxProvider.DeleteStore(_createdMessages);
+ }
+}
diff --git a/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Sync/When_Adding_A_Message_It_Should_Be_Stored_With_All_Properties.cs b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Sync/When_Adding_A_Message_It_Should_Be_Stored_With_All_Properties.cs
new file mode 100644
index 0000000000..7ddae5c64c
--- /dev/null
+++ b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Sync/When_Adding_A_Message_It_Should_Be_Stored_With_All_Properties.cs
@@ -0,0 +1,105 @@
+#region Licence
+
+/* The MIT License (MIT)
+Copyright © 2014 Ian Cooper
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE. */
+
+#endregion
+
+//
+// This file is auto-generated by Paramore.Brighter.Test.Generator
+//
+
+using Xunit;
+using System;
+using System.Collections.Generic;
+
+namespace Paramore.Brighter.Oracle.Tests.Outbox.Text.Sync;
+
+[Trait("Category", "Oracle")]
+public class WhenAddingAMessageItShouldBeStoredWithAllProperties : IDisposable
+{
+ private readonly IAmAnOutboxProviderSync _outboxProvider;
+ private readonly IAmAMessageBuilder _messageBuilder;
+ private List _createdMessages = [];
+
+ public WhenAddingAMessageItShouldBeStoredWithAllProperties()
+ {
+ _outboxProvider = new OracleTextOutboxProvider();
+ _outboxProvider.CreateStore();
+
+ _messageBuilder = new DefaultMessageBuilder();
+ }
+
+ [Fact]
+ public void When_Adding_A_Message_It_Should_Be_Stored_With_All_Properties()
+ {
+ // Arrange
+ var context = new RequestContext();
+ var message = _messageBuilder.Build();
+
+ _createdMessages.Add(message);
+
+ // Act
+ var outbox = _outboxProvider.CreateOutbox();
+ outbox.Add(message, context);
+
+ var storedMessage = outbox.Get(message.Id, context);
+
+ // Assert
+ Assert.Equal(message.Body.Value, storedMessage.Body.Value);
+
+ //should read the header from the sql outbox
+ Assert.Equal(message.Header.Topic, storedMessage.Header.Topic);
+ Assert.Equal(message.Header.MessageType, storedMessage.Header.MessageType);
+ Assert.Equal(message.Header.TimeStamp, storedMessage.Header.TimeStamp, TimeSpan.FromSeconds(1));
+ Assert.Equal(0, storedMessage.Header.HandledCount); // -- should be zero when read from outbox
+ Assert.Equal(TimeSpan.Zero, storedMessage.Header.Delayed); // -- should be zero when read from outbox
+ Assert.Equal(message.Header.CorrelationId, storedMessage.Header.CorrelationId);
+ Assert.Equal(message.Header.ReplyTo, storedMessage.Header.ReplyTo);
+ Assert.StartsWith(message.Header.ContentType.ToString(), storedMessage.Header.ContentType.ToString());
+ Assert.Equal(message.Header.PartitionKey, storedMessage.Header.PartitionKey);
+
+ //Bag serialization
+ Assert.Equal(message.Header.Bag.Count, storedMessage.Header.Bag.Count);
+ foreach (var (key, val) in message.Header.Bag)
+ {
+ Assert.Contains(key, storedMessage.Header.Bag);
+ Assert.Equal(val, storedMessage.Header.Bag[key].ToString());
+ }
+
+ //Asserts for workflow properties
+ Assert.Equal(message.Header.WorkflowId, storedMessage.Header.WorkflowId);
+ Assert.Equal(message.Header.JobId, storedMessage.Header.JobId);
+
+ // new fields assertions
+ Assert.Equal(message.Header.Source, storedMessage.Header.Source);
+ Assert.Equal(message.Header.Type, storedMessage.Header.Type);
+ Assert.Equal(message.Header.DataSchema, storedMessage.Header.DataSchema);
+ Assert.Equal(message.Header.Subject, storedMessage.Header.Subject);
+ Assert.Equal(message.Header.TraceParent, storedMessage.Header.TraceParent);
+ Assert.Equal(message.Header.TraceState, storedMessage.Header.TraceState);
+ }
+
+ public void Dispose()
+ {
+ _outboxProvider.DeleteStore(_createdMessages);
+ }
+}
diff --git a/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Sync/When_Adding_A_Message_Within_Transaction_And_Rollback_It_Should_Not_Be_Stored.cs b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Sync/When_Adding_A_Message_Within_Transaction_And_Rollback_It_Should_Not_Be_Stored.cs
new file mode 100644
index 0000000000..bd5a3016bd
--- /dev/null
+++ b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Sync/When_Adding_A_Message_Within_Transaction_And_Rollback_It_Should_Not_Be_Stored.cs
@@ -0,0 +1,79 @@
+#region Licence
+
+/* The MIT License (MIT)
+Copyright © 2014 Ian Cooper
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE. */
+
+#endregion
+
+//
+// This file is auto-generated by Paramore.Brighter.Test.Generator
+//
+
+using Xunit;
+using System;
+using System.Collections.Generic;
+
+namespace Paramore.Brighter.Oracle.Tests.Outbox.Text.Sync;
+
+[Trait("Category", "Oracle")]
+public class WhenAddingAMessageWithinTransactionAndRollbackItShouldNotBeStored : IDisposable
+{
+ private readonly IAmAnOutboxProviderSync _outboxProvider;
+ private readonly IAmAMessageBuilder _messageBuilder;
+ private List _createdMessages = [];
+
+ public WhenAddingAMessageWithinTransactionAndRollbackItShouldNotBeStored()
+ {
+ _outboxProvider = new OracleTextOutboxProvider();
+ _outboxProvider.CreateStore();
+
+ _messageBuilder = new DefaultMessageBuilder();
+ }
+
+ [Fact]
+ public void When_Adding_A_Message_Within_Transaction_And_Rollback_It_Should_Not_Be_Stored()
+ {
+ // Arrange
+ var outbox = _outboxProvider.CreateOutbox();
+
+ var transaction = _outboxProvider.CreateTransactionProvider();
+ _ = transaction.GetTransaction();
+
+ var context = new RequestContext();
+ var message = _messageBuilder.Build();
+
+ _createdMessages.Add(message);
+
+ // Act
+ outbox.Add(message, context, transactionProvider: transaction);
+ transaction.Rollback();
+
+ var storedMessage = outbox.Get(message.Id, context);
+
+ // Assert
+ Assert.Equal(MessageType.MT_NONE, storedMessage.Header.MessageType);
+ }
+
+ public void Dispose()
+ {
+ _outboxProvider.DeleteStore(_createdMessages);
+ }
+}
diff --git a/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Sync/When_Adding_A_Message_Within_Transaction_It_Should_Be_Stored_After_Commit.cs b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Sync/When_Adding_A_Message_Within_Transaction_It_Should_Be_Stored_After_Commit.cs
new file mode 100644
index 0000000000..103e71d288
--- /dev/null
+++ b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Sync/When_Adding_A_Message_Within_Transaction_It_Should_Be_Stored_After_Commit.cs
@@ -0,0 +1,110 @@
+#region Licence
+
+/* The MIT License (MIT)
+Copyright © 2014 Ian Cooper
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE. */
+
+#endregion
+
+//
+// This file is auto-generated by Paramore.Brighter.Test.Generator
+//
+
+using Xunit;
+using System;
+using System.Collections.Generic;
+
+namespace Paramore.Brighter.Oracle.Tests.Outbox.Text.Sync;
+
+[Trait("Category", "Oracle")]
+public class WhenAddingAMessageWithinTransactionItShouldBeStoredAfterCommit : IDisposable
+{
+ private readonly IAmAnOutboxProviderSync _outboxProvider;
+ private readonly IAmAMessageBuilder _messageBuilder;
+ private List _createdMessages = [];
+
+ public WhenAddingAMessageWithinTransactionItShouldBeStoredAfterCommit()
+ {
+ _outboxProvider = new OracleTextOutboxProvider();
+ _outboxProvider.CreateStore();
+
+ _messageBuilder = new DefaultMessageBuilder();
+ }
+
+ [Fact]
+ public void When_Adding_A_Message_Within_Transaction_It_Should_Be_Stored_After_Commit()
+ {
+ // Arrange
+ var outbox = _outboxProvider.CreateOutbox();
+
+ var transaction = _outboxProvider.CreateTransactionProvider();
+ _ = transaction.GetTransaction();
+
+ var context = new RequestContext();
+ var message = _messageBuilder.Build();
+
+ _createdMessages.Add(message );
+
+ // Act
+ outbox.Add(message, context, transactionProvider: transaction);
+ transaction.Commit();
+
+ var storedMessage = outbox.Get(message.Id, context);
+
+ // Assert
+ Assert.Equal(message.Body.Value, storedMessage.Body.Value);
+
+ //should read the header from the sql outbox
+ Assert.Equal(message.Header.Topic, storedMessage.Header.Topic);
+ Assert.Equal(message.Header.MessageType, storedMessage.Header.MessageType);
+ Assert.Equal(message.Header.TimeStamp, storedMessage.Header.TimeStamp, TimeSpan.FromSeconds(1));
+ Assert.Equal(0, storedMessage.Header.HandledCount); // -- should be zero when read from outbox
+ Assert.Equal(TimeSpan.Zero, storedMessage.Header.Delayed); // -- should be zero when read from outbox
+ Assert.Equal(message.Header.CorrelationId, storedMessage.Header.CorrelationId);
+ Assert.Equal(message.Header.ReplyTo, storedMessage.Header.ReplyTo);
+ Assert.StartsWith(message.Header.ContentType.ToString(), storedMessage.Header.ContentType.ToString());
+ Assert.Equal(message.Header.PartitionKey, storedMessage.Header.PartitionKey);
+
+ //Bag serialization
+ Assert.Equal(message.Header.Bag.Count, storedMessage.Header.Bag.Count);
+ foreach (var (key, val) in message.Header.Bag)
+ {
+ Assert.Contains(key, storedMessage.Header.Bag);
+ Assert.Equal(val, storedMessage.Header.Bag[key].ToString());
+ }
+
+ //Asserts for workflow properties
+ Assert.Equal(message.Header.WorkflowId, storedMessage.Header.WorkflowId);
+ Assert.Equal(message.Header.JobId, storedMessage.Header.JobId);
+
+ // new fields assertions
+ Assert.Equal(message.Header.Source, storedMessage.Header.Source);
+ Assert.Equal(message.Header.Type, storedMessage.Header.Type);
+ Assert.Equal(message.Header.DataSchema, storedMessage.Header.DataSchema);
+ Assert.Equal(message.Header.Subject, storedMessage.Header.Subject);
+ Assert.Equal(message.Header.TraceParent, storedMessage.Header.TraceParent);
+ Assert.Equal(message.Header.TraceState, storedMessage.Header.TraceState);
+ }
+
+ public void Dispose()
+ {
+ _outboxProvider.DeleteStore(_createdMessages);
+ }
+}
diff --git a/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Sync/When_Deleting_Multiple_Messages_They_Should_Be_Removed_From_Outbox.cs b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Sync/When_Deleting_Multiple_Messages_They_Should_Be_Removed_From_Outbox.cs
new file mode 100644
index 0000000000..b0414a2eff
--- /dev/null
+++ b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Sync/When_Deleting_Multiple_Messages_They_Should_Be_Removed_From_Outbox.cs
@@ -0,0 +1,84 @@
+#region Licence
+
+/* The MIT License (MIT)
+Copyright © 2014 Ian Cooper
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE. */
+
+#endregion
+
+//
+// This file is auto-generated by Paramore.Brighter.Test.Generator
+//
+
+using Xunit;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Paramore.Brighter.Oracle.Tests.Outbox.Text.Sync;
+
+[Trait("Category", "Oracle")]
+public class WhenDeletingMultipleMessagesTheyShouldBeRemovedFromOutbox : IDisposable
+{
+ private readonly IAmAnOutboxProviderSync _outboxProvider;
+ private List _createdMessages = [];
+
+ public WhenDeletingMultipleMessagesTheyShouldBeRemovedFromOutbox()
+ {
+ _outboxProvider = new OracleTextOutboxProvider();
+ _outboxProvider.CreateStore();
+ }
+
+ [Fact]
+ public void When_Deleting_Multiple_Messages_They_Should_Be_Removed_From_Outbox()
+ {
+ // Arrange
+ var context = new RequestContext();
+ var firstMessage = new DefaultMessageBuilder().Build();
+ var secondMessage = new DefaultMessageBuilder().Build();
+ var thirdMessage = new DefaultMessageBuilder().Build();
+
+ _createdMessages.Add(firstMessage);
+ _createdMessages.Add(secondMessage);
+ _createdMessages.Add(thirdMessage);
+
+ // Act
+ var outbox = _outboxProvider.CreateOutbox();
+ outbox.Add(firstMessage, context);
+ outbox.Add(secondMessage, context);
+ outbox.Add(thirdMessage, context);
+
+ outbox.Delete([firstMessage.Id, secondMessage.Id, thirdMessage.Id], context);
+
+ // Assert
+ var messages = outbox
+ .OutstandingMessages(TimeSpan.Zero, context)
+ .ToArray();
+
+ Assert.DoesNotContain(firstMessage.Id, messages.Select(x => x.Id));
+ Assert.DoesNotContain(secondMessage.Id, messages.Select(x => x.Id));
+ Assert.DoesNotContain(thirdMessage.Id, messages.Select(x => x.Id));
+ }
+
+ public void Dispose()
+ {
+ _outboxProvider.DeleteStore(_createdMessages);
+ }
+}
diff --git a/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Sync/When_Deleting_One_Message_It_Should_Be_Removed_From_Outbox.cs b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Sync/When_Deleting_One_Message_It_Should_Be_Removed_From_Outbox.cs
new file mode 100644
index 0000000000..07758291b4
--- /dev/null
+++ b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Sync/When_Deleting_One_Message_It_Should_Be_Removed_From_Outbox.cs
@@ -0,0 +1,84 @@
+#region Licence
+
+/* The MIT License (MIT)
+Copyright © 2014 Ian Cooper
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE. */
+
+#endregion
+
+//
+// This file is auto-generated by Paramore.Brighter.Test.Generator
+//
+
+using Xunit;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Paramore.Brighter.Oracle.Tests.Outbox.Text.Sync;
+
+[Trait("Category", "Oracle")]
+public class WhenDeletingOneMessageItShouldBeRemovedFromOutbox : IDisposable
+{
+ private readonly IAmAnOutboxProviderSync _outboxProvider;
+ private List _createdMessages = [];
+
+ public WhenDeletingOneMessageItShouldBeRemovedFromOutbox()
+ {
+ _outboxProvider = new OracleTextOutboxProvider();
+ _outboxProvider.CreateStore();
+ }
+
+ [Fact]
+ public void When_Deleting_One_Message_It_Should_Be_Removed_From_Outbox()
+ {
+ // Arrange
+ var context = new RequestContext();
+ var firstMessage = new DefaultMessageBuilder().Build();
+ var secondMessage = new DefaultMessageBuilder().Build();
+ var thirdMessage = new DefaultMessageBuilder().Build();
+
+ _createdMessages.Add(firstMessage);
+ _createdMessages.Add(secondMessage);
+ _createdMessages.Add(thirdMessage);
+
+ // Act
+ var outbox = _outboxProvider.CreateOutbox();
+ outbox.Add(firstMessage, context);
+ outbox.Add(secondMessage, context);
+ outbox.Add(thirdMessage, context);
+
+ outbox.Delete([firstMessage.Id], context);
+
+ // Assert
+ var messages = outbox
+ .OutstandingMessages(TimeSpan.Zero, context)
+ .ToArray();
+
+ Assert.DoesNotContain(firstMessage.Id, messages.Select(x => x.Id));
+ Assert.Contains(secondMessage.Id, messages.Select(x => x.Id));
+ Assert.Contains(thirdMessage.Id, messages.Select(x => x.Id));
+ }
+
+ public void Dispose()
+ {
+ _outboxProvider.DeleteStore(_createdMessages);
+ }
+}
diff --git a/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Sync/When_Retrieving_A_Message_By_Id_It_Should_Return_The_Correct_Message.cs b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Sync/When_Retrieving_A_Message_By_Id_It_Should_Return_The_Correct_Message.cs
new file mode 100644
index 0000000000..baec27e975
--- /dev/null
+++ b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Sync/When_Retrieving_A_Message_By_Id_It_Should_Return_The_Correct_Message.cs
@@ -0,0 +1,109 @@
+#region Licence
+
+/* The MIT License (MIT)
+Copyright © 2014 Ian Cooper
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE. */
+
+#endregion
+
+//
+// This file is auto-generated by Paramore.Brighter.Test.Generator
+//
+
+using Xunit;
+using System;
+using System.Collections.Generic;
+
+namespace Paramore.Brighter.Oracle.Tests.Outbox.Text.Sync;
+
+[Trait("Category", "Oracle")]
+public class WhenRetrievingAMessageByIdItShouldReturnTheCorrectMessage : IDisposable
+{
+ private readonly IAmAnOutboxProviderSync _outboxProvider;
+ private List _createdMessages = [];
+
+ public WhenRetrievingAMessageByIdItShouldReturnTheCorrectMessage()
+ {
+ _outboxProvider = new OracleTextOutboxProvider();
+ _outboxProvider.CreateStore();
+ }
+
+ [Fact]
+ public void When_Retrieving_A_Message_By_Id_It_Should_Return_The_Correct_Message()
+ {
+ // Arrange
+ var context = new RequestContext();
+ var earliest = new DefaultMessageBuilder().Build();
+ var dispatched = new DefaultMessageBuilder().Build();
+ var undispatched = new DefaultMessageBuilder().Build();
+
+ _createdMessages.Add(earliest);
+ _createdMessages.Add(dispatched);
+ _createdMessages.Add(undispatched);
+
+ var outbox = _outboxProvider.CreateOutbox();
+ outbox.Add([earliest, dispatched, undispatched], context);
+ outbox.MarkDispatched(earliest.Id, context, DateTime.UtcNow.AddHours(-3));
+ outbox.MarkDispatched(dispatched.Id, context, DateTime.UtcNow.AddSeconds(-30));
+
+ // Act
+ var message = outbox.Get(dispatched.Id, context);
+
+ // Assert
+ Assert.NotNull(message);
+ Assert.Equal(message.Body.Value, dispatched.Body.Value);
+
+ //should read the header from the sql outbox
+ Assert.Equal(message.Header.Topic, dispatched.Header.Topic);
+ Assert.Equal(message.Header.MessageType, dispatched.Header.MessageType);
+ Assert.Equal(message.Header.TimeStamp, dispatched.Header.TimeStamp, TimeSpan.FromSeconds(1));
+ Assert.Equal(0, dispatched.Header.HandledCount); // -- should be zero when read from outbox
+ // Assert.Equal(TimeSpan.Zero, dispatched.Header.Delayed); // -- should be zero when read from outbox
+ Assert.Equal(message.Header.CorrelationId, dispatched.Header.CorrelationId);
+ Assert.Equal(message.Header.ReplyTo, dispatched.Header.ReplyTo);
+ Assert.StartsWith(message.Header.ContentType.MediaType, dispatched.Header.ContentType.ToString());
+ Assert.Equal(message.Header.PartitionKey, dispatched.Header.PartitionKey);
+
+ //Bag serialization
+ Assert.Equal(message.Header.Bag.Count, dispatched.Header.Bag.Count);
+ foreach (var (key, val) in message.Header.Bag)
+ {
+ Assert.Contains(key, dispatched.Header.Bag);
+ Assert.Equal(val.ToString(), dispatched.Header.Bag[key].ToString());
+ }
+
+ //Asserts for workflow properties
+ Assert.Equal(message.Header.WorkflowId, dispatched.Header.WorkflowId);
+ Assert.Equal(message.Header.JobId, dispatched.Header.JobId);
+
+ // new fields assertions
+ Assert.Equal(message.Header.Source, dispatched.Header.Source);
+ Assert.Equal(message.Header.Type, dispatched.Header.Type);
+ Assert.Equal(message.Header.DataSchema, dispatched.Header.DataSchema);
+ Assert.Equal(message.Header.Subject, dispatched.Header.Subject);
+ Assert.Equal(message.Header.TraceParent, dispatched.Header.TraceParent);
+ Assert.Equal(message.Header.TraceState, dispatched.Header.TraceState);
+ }
+
+ public void Dispose()
+ {
+ _outboxProvider.DeleteStore(_createdMessages);
+ }
+}
diff --git a/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Sync/When_Retrieving_A_Non_Existent_Message_It_Should_Return_Empty_Message.cs b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Sync/When_Retrieving_A_Non_Existent_Message_It_Should_Return_Empty_Message.cs
new file mode 100644
index 0000000000..6e8a4c3d57
--- /dev/null
+++ b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Sync/When_Retrieving_A_Non_Existent_Message_It_Should_Return_Empty_Message.cs
@@ -0,0 +1,70 @@
+#region Licence
+
+/* The MIT License (MIT)
+Copyright © 2014 Ian Cooper
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE. */
+
+#endregion
+
+//
+// This file is auto-generated by Paramore.Brighter.Test.Generator
+//
+
+using Xunit;
+using System;
+using System.Collections.Generic;
+
+namespace Paramore.Brighter.Oracle.Tests.Outbox.Text.Sync;
+
+[Trait("Category", "Oracle")]
+public class WhenRetrievingANonExistentMessageItShouldReturnEmptyMessage : IDisposable
+{
+ private readonly IAmAnOutboxProviderSync _outboxProvider;
+ private readonly IAmAMessageBuilder _messageBuilder;
+ private List _createdMessages = [];
+
+ public WhenRetrievingANonExistentMessageItShouldReturnEmptyMessage()
+ {
+ _outboxProvider = new OracleTextOutboxProvider();
+ _outboxProvider.CreateStore();
+
+ _messageBuilder = new DefaultMessageBuilder();
+ }
+
+ [Fact]
+ public void When_Retrieving_A_Non_Existent_Message_It_Should_Return_Empty_Message()
+ {
+ // Arrange
+ var context = new RequestContext();
+
+ var outbox = _outboxProvider.CreateOutbox();
+
+ // Act
+ var message = outbox.Get(Id.Random(), context);
+
+ // Assert
+ Assert.Equal(MessageType.MT_NONE, message.Header.MessageType);
+ }
+
+ public void Dispose()
+ {
+ _outboxProvider.DeleteStore(_createdMessages);
+ }
+}
diff --git a/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Sync/When_Retrieving_All_Messages_They_Should_Include_Dispatched_And_Undispatched.cs b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Sync/When_Retrieving_All_Messages_They_Should_Include_Dispatched_And_Undispatched.cs
new file mode 100644
index 0000000000..ea824132c6
--- /dev/null
+++ b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Sync/When_Retrieving_All_Messages_They_Should_Include_Dispatched_And_Undispatched.cs
@@ -0,0 +1,81 @@
+#region Licence
+
+/* The MIT License (MIT)
+Copyright © 2014 Ian Cooper
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE. */
+
+#endregion
+
+//
+// This file is auto-generated by Paramore.Brighter.Test.Generator
+//
+
+using Xunit;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Paramore.Brighter.Oracle.Tests.Outbox.Text.Sync;
+
+[Trait("Category", "Oracle")]
+public class WhenRetrievingAllMessagesTheyShouldIncludeDispatchedAndUndispatched : IDisposable
+{
+ private readonly IAmAnOutboxProviderSync _outboxProvider;
+ private List _createdMessages = [];
+
+ public WhenRetrievingAllMessagesTheyShouldIncludeDispatchedAndUndispatched()
+ {
+ _outboxProvider = new OracleTextOutboxProvider();
+ _outboxProvider.CreateStore();
+ }
+
+ [Fact]
+ public void When_Retrieving_All_Messages_They_Should_Include_Dispatched_And_Undispatched()
+ {
+ // Arrange
+ var context = new RequestContext();
+ var earliest = new DefaultMessageBuilder().Build();
+ var dispatched = new DefaultMessageBuilder().Build();
+ var undispatched = new DefaultMessageBuilder().Build();
+
+ _createdMessages.Add(earliest);
+ _createdMessages.Add(dispatched);
+ _createdMessages.Add(undispatched);
+
+ var outbox = _outboxProvider.CreateOutbox();
+ outbox.Add([earliest, dispatched, undispatched], context);
+ outbox.MarkDispatched(earliest.Id, context, DateTime.UtcNow.AddHours(-3));
+ outbox.MarkDispatched(dispatched.Id, context, DateTime.UtcNow.AddSeconds(-30));
+
+ // Act
+ var messages = _outboxProvider.GetAllMessages().ToArray();
+
+ // Assert
+ Assert.True(messages.Length >= 3, "Expecting at least 3 messages");
+ Assert.Contains(earliest.Id, messages.Select(x => x.Id));
+ Assert.Contains(dispatched.Id, messages.Select(x => x.Id));
+ Assert.Contains(undispatched.Id, messages.Select(x => x.Id));
+ }
+
+ public void Dispose()
+ {
+ _outboxProvider.DeleteStore(_createdMessages);
+ }
+}
diff --git a/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Sync/When_Retrieving_Dispatched_Messages_It_Should_Filter_By_Age.cs b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Sync/When_Retrieving_Dispatched_Messages_It_Should_Filter_By_Age.cs
new file mode 100644
index 0000000000..e22c9e7873
--- /dev/null
+++ b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Sync/When_Retrieving_Dispatched_Messages_It_Should_Filter_By_Age.cs
@@ -0,0 +1,92 @@
+#region Licence
+
+/* The MIT License (MIT)
+Copyright © 2014 Ian Cooper
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE. */
+
+#endregion
+
+//
+// This file is auto-generated by Paramore.Brighter.Test.Generator
+//
+
+using Xunit;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Paramore.Brighter.Oracle.Tests.Outbox.Text.Sync;
+
+[Trait("Category", "Oracle")]
+public class WhenRetrievingDispatchedMessagesItShouldFilterByAge : IDisposable
+{
+ private readonly IAmAnOutboxProviderSync _outboxProvider;
+ private List _createdMessages = [];
+
+ public WhenRetrievingDispatchedMessagesItShouldFilterByAge()
+ {
+ _outboxProvider = new OracleTextOutboxProvider();
+ _outboxProvider.CreateStore();
+ }
+
+ [Fact]
+ public void When_Retrieving_Dispatched_Messages_It_Should_Filter_By_Age()
+ {
+ // Arrange
+ var context = new RequestContext();
+ var earliest = new DefaultMessageBuilder().Build();
+ var dispatched = new DefaultMessageBuilder().Build();
+ var undispatched = new DefaultMessageBuilder().Build();
+
+ _createdMessages.Add(earliest);
+ _createdMessages.Add(dispatched);
+ _createdMessages.Add(undispatched);
+
+ var outbox = _outboxProvider.CreateOutbox();
+ outbox.Add([earliest, dispatched, undispatched], context);
+ outbox.MarkDispatched(earliest.Id, context, DateTime.UtcNow.AddHours(-3));
+ outbox.MarkDispatched(dispatched.Id, context, DateTime.UtcNow.AddSeconds(-30));
+
+ // Act
+ var allDispatched = outbox.DispatchedMessages(TimeSpan.Zero, context).ToArray();
+ var messagesOverAnHour = outbox.DispatchedMessages(TimeSpan.FromHours(1), context).ToArray();
+ var messagesOver4Hours = outbox.DispatchedMessages(TimeSpan.FromHours(4), context).ToArray();
+
+ // Assert
+ Assert.True(allDispatched.Length >= 2, "Expecting at least 2 messages");
+ Assert.Contains(earliest.Id, allDispatched.Select(x => x.Id));
+ Assert.Contains(dispatched.Id, allDispatched.Select(x => x.Id));
+ Assert.DoesNotContain(undispatched.Id, allDispatched.Select(x => x.Id));
+
+ Assert.True(messagesOverAnHour.Length >= 1, "Expecting at least 1 message");
+ Assert.Contains(earliest.Id, messagesOverAnHour.Select(x => x.Id));
+ Assert.DoesNotContain(dispatched.Id, messagesOverAnHour.Select(x => x.Id));
+ Assert.DoesNotContain(undispatched.Id, messagesOverAnHour.Select(x => x.Id));
+
+ Assert.DoesNotContain(earliest.Id, messagesOver4Hours.Select(x => x.Id));
+ Assert.DoesNotContain(dispatched.Id, messagesOver4Hours.Select(x => x.Id));
+ Assert.DoesNotContain(undispatched.Id, messagesOver4Hours.Select(x => x.Id));
+ }
+
+ public void Dispose()
+ {
+ _outboxProvider.DeleteStore(_createdMessages);
+ }
+}
diff --git a/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Sync/When_Retrieving_Messages_By_Ids_It_Should_Return_Only_Requested_Messages.cs b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Sync/When_Retrieving_Messages_By_Ids_It_Should_Return_Only_Requested_Messages.cs
new file mode 100644
index 0000000000..124ccacd14
--- /dev/null
+++ b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Sync/When_Retrieving_Messages_By_Ids_It_Should_Return_Only_Requested_Messages.cs
@@ -0,0 +1,83 @@
+#region Licence
+
+/* The MIT License (MIT)
+Copyright © 2014 Ian Cooper
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE. */
+
+#endregion
+
+//
+// This file is auto-generated by Paramore.Brighter.Test.Generator
+//
+
+using Xunit;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Paramore.Brighter.Oracle.Tests.Outbox.Text.Sync;
+
+[Trait("Category", "Oracle")]
+public class WhenRetrievingMessagesByIdsItShouldReturnOnlyRequestedMessages : IDisposable
+{
+ private readonly IAmAnOutboxProviderSync _outboxProvider;
+ private List _createdMessages = [];
+
+ public WhenRetrievingMessagesByIdsItShouldReturnOnlyRequestedMessages()
+ {
+ _outboxProvider = new OracleTextOutboxProvider();
+ _outboxProvider.CreateStore();
+ }
+
+ [Fact]
+ public void When_Retrieving_Messages_By_Ids_It_Should_Return_Only_Requested_Messages()
+ {
+ // Arrange
+ var context = new RequestContext();
+ var earliest = new DefaultMessageBuilder().Build();
+ var dispatched = new DefaultMessageBuilder().Build();
+ var undispatched = new DefaultMessageBuilder().Build();
+
+ _createdMessages.Add(earliest);
+ _createdMessages.Add(dispatched);
+ _createdMessages.Add(undispatched);
+
+ var outbox = _outboxProvider.CreateOutbox();
+ outbox.Add([earliest, dispatched, undispatched], context);
+ outbox.MarkDispatched(earliest.Id, context, DateTime.UtcNow.AddHours(-3));
+ outbox.MarkDispatched(dispatched.Id, context, DateTime.UtcNow.AddSeconds(-30));
+
+ // Act
+ var messages = outbox
+ .Get([earliest.Id, undispatched.Id], context)
+ .ToArray();
+
+ // Assert
+ Assert.Equal(2, messages.Length);
+ Assert.Contains(earliest.Id, messages.Select(x => x.Id));
+ Assert.DoesNotContain(dispatched.Id, messages.Select(x => x.Id));
+ Assert.Contains(undispatched.Id, messages.Select(x => x.Id));
+ }
+
+ public void Dispose()
+ {
+ _outboxProvider.DeleteStore(_createdMessages);
+ }
+}
diff --git a/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Sync/When_Retrieving_Outstanding_Messages_It_Should_Filter_By_Age.cs b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Sync/When_Retrieving_Outstanding_Messages_It_Should_Filter_By_Age.cs
new file mode 100644
index 0000000000..6784528dc2
--- /dev/null
+++ b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Sync/When_Retrieving_Outstanding_Messages_It_Should_Filter_By_Age.cs
@@ -0,0 +1,92 @@
+#region Licence
+
+/* The MIT License (MIT)
+Copyright © 2014 Ian Cooper
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE. */
+
+#endregion
+
+//
+// This file is auto-generated by Paramore.Brighter.Test.Generator
+//
+
+using Xunit;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Paramore.Brighter.Oracle.Tests.Outbox.Text.Sync;
+
+[Trait("Category", "Oracle")]
+public class WhenRetrievingOutstandingMessagesItShouldFilterByAge : IDisposable
+{
+ private readonly IAmAnOutboxProviderSync _outboxProvider;
+ private List _createdMessages = [];
+
+ public WhenRetrievingOutstandingMessagesItShouldFilterByAge()
+ {
+ _outboxProvider = new OracleTextOutboxProvider();
+ _outboxProvider.CreateStore();
+ }
+
+ [Fact]
+ public void When_Retrieving_Outstanding_Messages_It_Should_Filter_By_Age()
+ {
+ // Arrange
+ var context = new RequestContext();
+ var earliest = new DefaultMessageBuilder().Build();
+ var dispatched = new DefaultMessageBuilder().Build();
+ var undispatched = new DefaultMessageBuilder().Build();
+
+ _createdMessages.Add(earliest);
+ _createdMessages.Add(dispatched);
+ _createdMessages.Add(undispatched);
+
+ var outbox = _outboxProvider.CreateOutbox();
+ outbox.Add([earliest, dispatched, undispatched], context);
+ outbox.MarkDispatched(earliest.Id, context, DateTime.UtcNow.AddHours(-3));
+ outbox.MarkDispatched(dispatched.Id, context, DateTime.UtcNow.AddSeconds(-30));
+
+ // Act
+ var allDispatched = outbox.DispatchedMessages(TimeSpan.Zero, context).ToArray();
+ var messagesOverAnHour = outbox.DispatchedMessages(TimeSpan.FromHours(1), context).ToArray();
+ var messagesOver4Hours = outbox.DispatchedMessages(TimeSpan.FromHours(4), context).ToArray();
+
+ // Assert
+ Assert.True(allDispatched.Length >= 2, "Expecting at least 2 messages");
+ Assert.Contains(earliest.Id, allDispatched.Select(x => x.Id));
+ Assert.Contains(dispatched.Id, allDispatched.Select(x => x.Id));
+ Assert.DoesNotContain(undispatched.Id, allDispatched.Select(x => x.Id));
+
+ Assert.True(messagesOverAnHour.Length >= 1, "Expecting at least 1 message");
+ Assert.Contains(earliest.Id, messagesOverAnHour.Select(x => x.Id));
+ Assert.DoesNotContain(dispatched.Id, messagesOverAnHour.Select(x => x.Id));
+ Assert.DoesNotContain(undispatched.Id, messagesOverAnHour.Select(x => x.Id));
+
+ Assert.DoesNotContain(earliest.Id, messagesOver4Hours.Select(x => x.Id));
+ Assert.DoesNotContain(dispatched.Id, messagesOver4Hours.Select(x => x.Id));
+ Assert.DoesNotContain(undispatched.Id, messagesOver4Hours.Select(x => x.Id));
+ }
+
+ public void Dispose()
+ {
+ _outboxProvider.DeleteStore(_createdMessages);
+ }
+}
diff --git a/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/OracleTextOutboxProvider.cs b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/OracleTextOutboxProvider.cs
new file mode 100644
index 0000000000..0dab362b00
--- /dev/null
+++ b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/OracleTextOutboxProvider.cs
@@ -0,0 +1,80 @@
+using System.Collections.Generic;
+using System.Data.Common;
+using System.Threading.Tasks;
+using Oracle.ManagedDataAccess.Client;
+using Paramore.Brighter.Oracle.Tests.Outbox.Text.Async;
+using Paramore.Brighter.Oracle.Tests.Outbox.Text.Sync;
+using Paramore.Brighter.Outbox.Oracle;
+
+namespace Paramore.Brighter.Oracle.Tests.Outbox.Text;
+
+public class OracleTextOutboxProvider : IAmAnOutboxProviderSync, IAmAnOutboxProviderAsync
+{
+ private readonly RelationalDatabaseConfiguration _configuration = new(
+ Const.DefaultConnectingString,
+ outBoxTableName: $"{Const.TablePrefix}{Uuid.New():N}",
+ binaryMessagePayload: false);
+
+ public IAmAnOutboxSync CreateOutbox()
+ {
+ return new OracleOutbox(_configuration);
+ }
+
+ public IAmAnOutboxAsync CreateOutboxAsync()
+ {
+ return new OracleOutbox(_configuration);
+ }
+
+ public void CreateStore()
+ {
+ using var connection = new OracleConnection(_configuration.ConnectionString);
+ connection.Open();
+ using var command = connection.CreateCommand();
+ command.CommandText = OracleOutboxBuilder.GetDDL(_configuration.OutBoxTableName);
+ command.ExecuteNonQuery();
+ }
+
+ public async Task CreateStoreAsync()
+ {
+ await using var connection = new OracleConnection(_configuration.ConnectionString);
+ await connection.OpenAsync();
+ await using var command = connection.CreateCommand();
+ command.CommandText = OracleOutboxBuilder.GetDDL(_configuration.OutBoxTableName);
+ await command.ExecuteNonQueryAsync();
+ }
+
+ public IAmABoxTransactionProvider CreateTransactionProvider()
+ {
+ return new OracleTransactionProvider(_configuration);
+ }
+
+ public void DeleteStore(IEnumerable messages)
+ {
+ using var connection = new OracleConnection(_configuration.ConnectionString);
+ connection.Open();
+ using var command = connection.CreateCommand();
+ command.CommandText = $"DROP TABLE {_configuration.OutBoxTableName}";
+ command.ExecuteNonQuery();
+ }
+
+ public async Task DeleteStoreAsync(IEnumerable messages)
+ {
+ await using var connection = new OracleConnection(_configuration.ConnectionString);
+ await connection.OpenAsync();
+ await using var command = connection.CreateCommand();
+ command.CommandText = $"DROP TABLE {_configuration.OutBoxTableName}";
+ await command.ExecuteNonQueryAsync();
+ }
+
+ public IEnumerable GetAllMessages()
+ {
+ var outbox = new OracleOutbox(_configuration);
+ return outbox.Get(new RequestContext());
+ }
+
+ public async Task> GetAllMessagesAsync()
+ {
+ var outbox = new OracleOutbox(_configuration);
+ return await outbox.GetAsync(new RequestContext());
+ }
+}
diff --git a/tests/Paramore.Brighter.Oracle.Tests/Paramore.Brighter.Oracle.Tests.csproj b/tests/Paramore.Brighter.Oracle.Tests/Paramore.Brighter.Oracle.Tests.csproj
new file mode 100644
index 0000000000..faa756bc67
--- /dev/null
+++ b/tests/Paramore.Brighter.Oracle.Tests/Paramore.Brighter.Oracle.Tests.csproj
@@ -0,0 +1,37 @@
+
+
+
+ $(BrighterTestTargetFrameworks)
+ false
+ enable
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/Paramore.Brighter.Oracle.Tests/test-configuration.json b/tests/Paramore.Brighter.Oracle.Tests/test-configuration.json
new file mode 100644
index 0000000000..5964a998c6
--- /dev/null
+++ b/tests/Paramore.Brighter.Oracle.Tests/test-configuration.json
@@ -0,0 +1,17 @@
+{
+ "Namespace": "Paramore.Brighter.Oracle.Tests",
+ "Outboxes": {
+ "Text": {
+ "Transaction": "System.Data.Common.DbTransaction",
+ "OutboxProvider": "OracleTextOutboxProvider",
+ "Category": "Oracle",
+ "CollectionName": "OracleTextOutbox"
+ },
+ "Binary": {
+ "Transaction": "System.Data.Common.DbTransaction",
+ "OutboxProvider": "OracleBinaryOutboxProvider",
+ "Category": "Oracle",
+ "CollectionName": "OracleBinaryOutbox"
+ }
+ }
+}