├── artwork
├── freakout.png
├── freakout.webp
├── transparent_2024-04-11T18-52-17.png
└── transparent_2024-04-11T18-52-17_cropped.png
├── tools
└── NuGet
│ └── nuget.exe
├── Freakout
├── Internals
│ ├── EmptyFreakoutContext.cs
│ ├── InternalsVisibleTo.cs
│ ├── IsExternalInit.cs
│ ├── DelegatingCommandHandler.cs
│ ├── AsyncLocalFreakoutContextAccessor.cs
│ ├── InternalExtensions.cs
│ ├── DefaultBatchDispatcher.cs
│ ├── CommandDispatcher.cs
│ ├── Dispatchers
│ │ ├── CompiledExpressionCommandDispatcher.cs
│ │ └── IlEmitCommandDispatcher.cs
│ ├── TypeExtensions.cs
│ └── FreakoutBackgroundService.cs
├── IFreakoutContext.cs
├── OutboxCommand.cs
├── HeaderKeys.cs
├── IContextHooks.cs
├── IOutboxCommandStore.cs
├── IBatchDispatcher.cs
├── ICommandDispatcher.cs
├── ICommandHandler.cs
├── IOutbox.cs
├── ICommandSerializer.cs
├── IFreakoutContextAccessor.cs
├── Serialization
│ ├── HeaderSerializer.cs
│ └── SystemTextJsonCommandSerializer.cs
├── Freakout.csproj
├── PendingOutboxCommand.cs
├── FreakoutConfiguration.cs
├── FreakoutContextScope.cs
├── OutboxCommandBatch.cs
└── Config
│ └── FreakoutServiceCollectionExtensions.cs
├── Freakout.MsSql
├── Internals
│ ├── InternalsVisibleTo.cs
│ ├── IsExternalInit.cs
│ ├── InternalExtensions.cs
│ ├── MsSqlOutbox.cs
│ └── MsSqlOutboxCommandStore.cs
├── PostgresCommandHelper.cs
├── MsSqlFreakoutContext.cs
├── Freakout.MsSql.csproj
├── MsSqlFreakoutConfiguration.cs
└── FreakoutSqlConnectionExtensions.cs
├── Freakout.NpgSql
├── Internals
│ ├── IsExternalInit.cs
│ ├── InternalExtensions.cs
│ ├── NpgSqlOutbox.cs
│ └── NpgSqlOutboxCommandStore.cs
├── NpgsqlFreakoutContext.cs
├── Freakout.NpgSql.csproj
├── NpgSqlFreakoutConfiguration.cs
└── FreakoutNpgsqlConnectionExtensions.cs
├── Freakout.Testing
├── Internals
│ ├── IsExternalInit.cs
│ ├── InMemOutboxCommandStore.cs
│ ├── InMemOutboxDecorator.cs
│ └── InMemOutbox.cs
├── InMemOutboxCommand.cs
├── InMemFreakoutContext.cs
├── Freakout.Testing.csproj
└── InMemFreakoutConfiguration.cs
├── Freakout.MsSql.Tests
├── Contracts
│ ├── MsSqlNormalTests.cs
│ ├── MsSqlExtensibilityTests.cs
│ └── MsSqlOutboxCommandStoreTests.cs
├── Freakout.MsSql.Tests.csproj
├── MsSqlTestHelper.cs
├── MsSqlFreakoutSystemFactory.cs
├── ThisIsWhatWeWant.cs
└── SimpleSqlServerPoc.cs
├── Freakout.NpgSql.Tests
├── Contracts
│ ├── NpgSqlNormalTests.cs
│ ├── NpgsqlExtensibilityTests.cs
│ └── NpgsqlOutboxCommandStoreTests.cs
├── Freakout.NpgSql.Tests.csproj
├── NpgSqlTestHelper.cs
└── NpgSqlFreakoutSystemFactory.cs
├── Freakout.Marten
├── FreakoutMartenConfigurationExtensions.cs
├── Freakout.Marten.csproj
└── MartenOutboxExtensions.cs
├── Freakout.Tests
├── Contracts
│ ├── IFreakoutSystemFactory.cs
│ ├── AbstractFreakoutSystemFactory.cs
│ ├── FreakoutSystem.cs
│ ├── ExtensibilityTests.cs
│ ├── OutboxCommandStoreTests.cs
│ └── NormalTests.cs
├── Freakout.Tests.csproj
├── TestExtensions.cs
├── TestFreakoutContextScope.cs
└── Dispatch
│ └── TestCommandDispatcher.cs
├── Freakout.sln.DotSettings
├── Freakout.Testing.Tests
├── Freakout.Testing.Tests.csproj
└── CheckHowItLooks.cs
├── scripts
├── build.cmd
├── push.cmd
└── release.cmd
├── LICENSE
├── CHANGELOG.md
├── README.md
├── Freakout.sln
└── .gitignore
/artwork/freakout.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rebus-org/Freakout/HEAD/artwork/freakout.png
--------------------------------------------------------------------------------
/artwork/freakout.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rebus-org/Freakout/HEAD/artwork/freakout.webp
--------------------------------------------------------------------------------
/tools/NuGet/nuget.exe:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rebus-org/Freakout/HEAD/tools/NuGet/nuget.exe
--------------------------------------------------------------------------------
/Freakout/Internals/EmptyFreakoutContext.cs:
--------------------------------------------------------------------------------
1 | namespace Freakout.Internals;
2 |
3 | class EmptyFreakoutContext : IFreakoutContext;
--------------------------------------------------------------------------------
/artwork/transparent_2024-04-11T18-52-17.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rebus-org/Freakout/HEAD/artwork/transparent_2024-04-11T18-52-17.png
--------------------------------------------------------------------------------
/Freakout.MsSql/Internals/InternalsVisibleTo.cs:
--------------------------------------------------------------------------------
1 | using System.Runtime.CompilerServices;
2 |
3 | [assembly: InternalsVisibleTo("Freakout.MsSql.Tests")]
--------------------------------------------------------------------------------
/artwork/transparent_2024-04-11T18-52-17_cropped.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rebus-org/Freakout/HEAD/artwork/transparent_2024-04-11T18-52-17_cropped.png
--------------------------------------------------------------------------------
/Freakout/IFreakoutContext.cs:
--------------------------------------------------------------------------------
1 | namespace Freakout;
2 |
3 | ///
4 | /// Marker interface for an ambient Freakout context
5 | ///
6 | public interface IFreakoutContext;
--------------------------------------------------------------------------------
/Freakout/Internals/InternalsVisibleTo.cs:
--------------------------------------------------------------------------------
1 | using System.Runtime.CompilerServices;
2 |
3 | [assembly: InternalsVisibleTo("Freakout.Tests")]
4 |
5 | [assembly: InternalsVisibleTo("Freakout.MsSql")]
--------------------------------------------------------------------------------
/Freakout/Internals/IsExternalInit.cs:
--------------------------------------------------------------------------------
1 | // ReSharper disable CheckNamespace
2 | // ReSharper disable UnusedMember.Global
3 | namespace System.Runtime.CompilerServices;
4 |
5 | class IsExternalInit;
6 |
--------------------------------------------------------------------------------
/Freakout.MsSql/Internals/IsExternalInit.cs:
--------------------------------------------------------------------------------
1 | // ReSharper disable CheckNamespace
2 | // ReSharper disable UnusedMember.Global
3 | namespace System.Runtime.CompilerServices;
4 |
5 | class IsExternalInit;
6 |
--------------------------------------------------------------------------------
/Freakout.NpgSql/Internals/IsExternalInit.cs:
--------------------------------------------------------------------------------
1 | // ReSharper disable CheckNamespace
2 | // ReSharper disable UnusedMember.Global
3 | namespace System.Runtime.CompilerServices;
4 |
5 | class IsExternalInit;
6 |
--------------------------------------------------------------------------------
/Freakout.Testing/Internals/IsExternalInit.cs:
--------------------------------------------------------------------------------
1 | // ReSharper disable CheckNamespace
2 | // ReSharper disable UnusedMember.Global
3 | namespace System.Runtime.CompilerServices;
4 |
5 | class IsExternalInit;
6 |
--------------------------------------------------------------------------------
/Freakout.MsSql/PostgresCommandHelper.cs:
--------------------------------------------------------------------------------
1 | namespace Freakout.MsSql;
2 |
3 | ///
4 | /// Useful bits of actual PostgreSQL command generation are made accessible here
5 | ///
6 | public static class PostgresCommandHelper
7 | {
8 |
9 | }
--------------------------------------------------------------------------------
/Freakout.MsSql.Tests/Contracts/MsSqlNormalTests.cs:
--------------------------------------------------------------------------------
1 | using Freakout.Tests.Contracts;
2 | using NUnit.Framework;
3 |
4 | namespace Freakout.MsSql.Tests.Contracts;
5 |
6 | [TestFixture]
7 | public class MsSqlNormalTests : NormalTests;
--------------------------------------------------------------------------------
/Freakout/OutboxCommand.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 |
3 | namespace Freakout;
4 |
5 | ///
6 | /// Raw store command before being persisted
7 | ///
8 | public record OutboxCommand(Dictionary Headers, byte[] Payload);
--------------------------------------------------------------------------------
/Freakout.NpgSql.Tests/Contracts/NpgSqlNormalTests.cs:
--------------------------------------------------------------------------------
1 | using Freakout.Tests.Contracts;
2 | using NUnit.Framework;
3 |
4 | namespace Freakout.NpgSql.Tests.Contracts;
5 |
6 | [TestFixture]
7 | public class NpgsqlNormalTests : NormalTests;
--------------------------------------------------------------------------------
/Freakout.MsSql.Tests/Contracts/MsSqlExtensibilityTests.cs:
--------------------------------------------------------------------------------
1 | using Freakout.Tests.Contracts;
2 | using NUnit.Framework;
3 |
4 | namespace Freakout.MsSql.Tests.Contracts;
5 |
6 | [TestFixture]
7 | public class MsSqlExtensibilityTests : ExtensibilityTests;
--------------------------------------------------------------------------------
/Freakout.NpgSql.Tests/Contracts/NpgsqlExtensibilityTests.cs:
--------------------------------------------------------------------------------
1 | using Freakout.Tests.Contracts;
2 | using NUnit.Framework;
3 |
4 | namespace Freakout.NpgSql.Tests.Contracts;
5 |
6 | [TestFixture]
7 | public class NpgsqlExtensibilityTests : ExtensibilityTests;
--------------------------------------------------------------------------------
/Freakout.MsSql.Tests/Contracts/MsSqlOutboxCommandStoreTests.cs:
--------------------------------------------------------------------------------
1 | using Freakout.Tests.Contracts;
2 | using NUnit.Framework;
3 |
4 | namespace Freakout.MsSql.Tests.Contracts;
5 |
6 | [TestFixture]
7 | public class MsSqlOutboxCommandStoreTests : OutboxCommandStoreTests;
--------------------------------------------------------------------------------
/Freakout.NpgSql.Tests/Contracts/NpgsqlOutboxCommandStoreTests.cs:
--------------------------------------------------------------------------------
1 | using Freakout.Tests.Contracts;
2 | using NUnit.Framework;
3 |
4 | namespace Freakout.NpgSql.Tests.Contracts;
5 |
6 | [TestFixture]
7 | public class NpgsqlOutboxCommandStoreTests : OutboxCommandStoreTests;
--------------------------------------------------------------------------------
/Freakout.Testing/InMemOutboxCommand.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 |
3 | namespace Freakout.Testing;
4 |
5 | ///
6 | /// Holds the information of an in-mem outbox command.
7 | ///
8 | public record InMemOutboxCommand(Dictionary Headers, object Command);
--------------------------------------------------------------------------------
/Freakout.Marten/FreakoutMartenConfigurationExtensions.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.DependencyInjection;
2 |
3 | namespace Freakout.Marten;
4 |
5 | public static class FreakoutMartenConfigurationExtensions
6 | {
7 | public static void AddFreakoutMartenIntegration(this IServiceCollection services)
8 | {
9 |
10 | }
11 | }
--------------------------------------------------------------------------------
/Freakout.Tests/Contracts/IFreakoutSystemFactory.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.DependencyInjection;
2 | using System;
3 |
4 | namespace Freakout.Tests.Contracts;
5 |
6 | public interface IFreakoutSystemFactory : IDisposable
7 | {
8 | FreakoutSystem Create(Action before = null, Action after = null);
9 | }
--------------------------------------------------------------------------------
/Freakout.Testing/Internals/InMemOutboxCommandStore.cs:
--------------------------------------------------------------------------------
1 | using System.Threading;
2 | using System.Threading.Tasks;
3 | #pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
4 |
5 | namespace Freakout.Testing.Internals;
6 |
7 | class InMemOutboxCommandStore : IOutboxCommandStore
8 | {
9 | public async Task GetPendingOutboxCommandsAsync(int commandProcessingBatchSize, CancellationToken cancellationToken = default) => OutboxCommandBatch.Empty;
10 | }
--------------------------------------------------------------------------------
/Freakout.sln.DotSettings:
--------------------------------------------------------------------------------
1 |
2 | server=(localdb)\MSSQLLocalDb; database=freakout_test; trusted_connection=true; encrypt=false
--------------------------------------------------------------------------------
/Freakout/Internals/DelegatingCommandHandler.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading;
3 | using System.Threading.Tasks;
4 |
5 | namespace Freakout.Internals;
6 |
7 | ///
8 | /// Built-in generic command handler that just delegates its invocation to the given function.
9 | ///
10 | class DelegatingCommandHandler(Func invoker) : ICommandHandler
11 | {
12 | public Task HandleAsync(TCommand command, CancellationToken cancellationToken) => invoker(command, cancellationToken);
13 | }
--------------------------------------------------------------------------------
/Freakout/HeaderKeys.cs:
--------------------------------------------------------------------------------
1 | namespace Freakout;
2 |
3 | ///
4 | /// Keys of headers that have special meaning in Freakout. Please use only if you know what you're doing ;)
5 | ///
6 | public static class HeaderKeys
7 | {
8 | ///
9 | /// Type information for the (de)serializer to use to be able to construct a command object
10 | ///
11 | public const string CommandType = "cmd-type";
12 |
13 | ///
14 | /// MIME type of the serialized payload
15 | ///
16 | public const string ContentType = "content-type";
17 | }
--------------------------------------------------------------------------------
/Freakout.MsSql/Internals/InternalExtensions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 |
4 | namespace Freakout.MsSql.Internals;
5 |
6 | static class InternalExtensions
7 | {
8 | public static void InsertInto(this Dictionary source, Dictionary target)
9 | {
10 | if (source == null) throw new ArgumentNullException(nameof(source));
11 | if (target == null) throw new ArgumentNullException(nameof(target));
12 |
13 | foreach (var kvp in source)
14 | {
15 | target[kvp.Key] = kvp.Value;
16 | }
17 | }
18 | }
--------------------------------------------------------------------------------
/Freakout.NpgSql/Internals/InternalExtensions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 |
4 | namespace Freakout.NpgSql.Internals;
5 |
6 | static class InternalExtensions
7 | {
8 | public static void InsertInto(this Dictionary source, Dictionary target)
9 | {
10 | if (source == null) throw new ArgumentNullException(nameof(source));
11 | if (target == null) throw new ArgumentNullException(nameof(target));
12 |
13 | foreach (var kvp in source)
14 | {
15 | target[kvp.Key] = kvp.Value;
16 | }
17 | }
18 | }
--------------------------------------------------------------------------------
/Freakout/IContextHooks.cs:
--------------------------------------------------------------------------------
1 | namespace Freakout;
2 |
3 | ///
4 | /// Can be implemented by to receive calls after being mounted as the ambient context
5 | /// and after being unmounted again.
6 | ///
7 | public interface IContextHooks : IFreakoutContext
8 | {
9 | ///
10 | /// Called after the context has been mounted as the ambient context by
11 | ///
12 | void Mounted();
13 |
14 | ///
15 | /// Called after the context has been unmounted again by
16 | ///
17 | void Unmounted();
18 | }
--------------------------------------------------------------------------------
/Freakout/IOutboxCommandStore.cs:
--------------------------------------------------------------------------------
1 | using System.Threading;
2 | using System.Threading.Tasks;
3 |
4 | namespace Freakout;
5 |
6 | ///
7 | /// Main store implementation. Must be implemented for the chosen type of technology chosen as the store
8 | ///
9 | public interface IOutboxCommandStore
10 | {
11 | ///
12 | /// Must return an "store batch", which is 0..n store commands and a "completion method" (i.e. a way of marking
13 | /// the contained store commands as handled).
14 | ///
15 | Task GetPendingOutboxCommandsAsync(int commandProcessingBatchSize, CancellationToken cancellationToken = default);
16 | }
--------------------------------------------------------------------------------
/Freakout.Testing.Tests/Freakout.Testing.Tests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net9.0
5 | enable
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/Freakout/IBatchDispatcher.cs:
--------------------------------------------------------------------------------
1 | using System.Threading;
2 | using System.Threading.Tasks;
3 |
4 | namespace Freakout;
5 |
6 | ///
7 | /// Interface of Freakout's batch dispatcher. This one basically defines what it means to process an and how it's done.
8 | ///
9 | public interface IBatchDispatcher
10 | {
11 | ///
12 | /// This method will be called by Freakout to process the . If it throws an exception,
13 | /// handling is considered as FAILED - if it doesn't, then it's considered SUCCESSFUL.
14 | ///
15 | Task ExecuteAsync(OutboxCommandBatch batch, CancellationToken cancellationToken = default);
16 | }
--------------------------------------------------------------------------------
/Freakout/ICommandDispatcher.cs:
--------------------------------------------------------------------------------
1 | using System.Threading;
2 | using System.Threading.Tasks;
3 |
4 | namespace Freakout;
5 |
6 | ///
7 | /// Interface of Freakout's command dispatcher. This one defines what it means to process an and how it's done.
8 | ///
9 | public interface ICommandDispatcher
10 | {
11 | ///
12 | /// This method will be called by Freakout to process the . If it throws an exception,
13 | /// handling is considered as FAILED - if it doesn't, then it's considered SUCCESSFUL.
14 | ///
15 | Task ExecuteAsync(OutboxCommand outboxCommand, CancellationToken cancellationToken = default);
16 | }
--------------------------------------------------------------------------------
/Freakout/ICommandHandler.cs:
--------------------------------------------------------------------------------
1 | using System.Threading;
2 | using System.Threading.Tasks;
3 |
4 | namespace Freakout;
5 |
6 | ///
7 | /// Freakout command handler marker interface. Used to enable a little bit of nudging via generics.
8 | ///
9 | public interface ICommandHandler { }
10 |
11 | ///
12 | /// Freakout command handler interface. Classes that implement this one or more times can be registered
13 | /// as command handlers and will get to handle commands.
14 | ///
15 | public interface ICommandHandler : ICommandHandler
16 | {
17 | ///
18 | /// Handler method that will be called by Freakout
19 | ///
20 | Task HandleAsync(TCommand command, CancellationToken cancellationToken);
21 | }
--------------------------------------------------------------------------------
/Freakout.Testing/InMemFreakoutContext.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Concurrent;
3 | using System.Collections.Generic;
4 |
5 | namespace Freakout.Testing;
6 |
7 | ///
8 | /// Implementation of that buffers messages in memory
9 | ///
10 | public class InMemFreakoutContext : IContextHooks
11 | {
12 | readonly ConcurrentQueue _commands = new();
13 |
14 | internal void Enlist(InMemOutboxCommand command) => _commands.Enqueue(command);
15 |
16 | internal Action> UnmountedCallback;
17 |
18 | ///
19 | public void Mounted()
20 | {
21 | }
22 |
23 | ///
24 | public void Unmounted() => UnmountedCallback?.Invoke(_commands);
25 | }
--------------------------------------------------------------------------------
/scripts/build.cmd:
--------------------------------------------------------------------------------
1 | @echo off
2 |
3 | set scriptsdir=%~dp0
4 | set root=%scriptsdir%\..
5 | set project=%1
6 | set version=%2
7 |
8 | if "%project%"=="" (
9 | echo Please invoke the build script with a project name as its first argument.
10 | echo.
11 | goto exit_fail
12 | )
13 |
14 | if "%version%"=="" (
15 | echo Please invoke the build script with a version as its second argument.
16 | echo.
17 | goto exit_fail
18 | )
19 |
20 | set Version=%version%
21 |
22 | pushd %root%
23 |
24 | dotnet restore --interactive
25 | if %ERRORLEVEL% neq 0 (
26 | popd
27 | goto exit_fail
28 | )
29 |
30 | dotnet build -c Release --no-restore
31 | if %ERRORLEVEL% neq 0 (
32 | popd
33 | goto exit_fail
34 | )
35 |
36 | popd
37 |
38 |
39 |
40 |
41 |
42 |
43 | goto exit_success
44 | :exit_fail
45 | exit /b 1
46 | :exit_success
--------------------------------------------------------------------------------
/Freakout/IOutbox.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Threading;
3 | using System.Threading.Tasks;
4 | // ReSharper disable UnusedMember.Global
5 |
6 | namespace Freakout;
7 |
8 | ///
9 | /// Main Freakout outbox command adder.
10 | ///
11 | public interface IOutbox
12 | {
13 | ///
14 | /// Adds a single outbox command to the outbox command store
15 | ///
16 | void AddOutboxCommand(object command, Dictionary headers = null, CancellationToken cancellationToken = default);
17 |
18 | ///
19 | /// Adds a single outbox command to the outbox command store
20 | ///
21 | Task AddOutboxCommandAsync(object command, Dictionary headers = null, CancellationToken cancellationToken = default);
22 | }
--------------------------------------------------------------------------------
/Freakout/ICommandSerializer.cs:
--------------------------------------------------------------------------------
1 | namespace Freakout;
2 |
3 | ///
4 | /// Command serializer. Must be capable of serializing all the relevant commands
5 | ///
6 | public interface ICommandSerializer
7 | {
8 | ///
9 | /// Serializes the given into an .
10 | /// Please note that the headers returned in the are highly likely
11 | /// to be important to the deserialization process, so please only tamper with them if you know what you're doing.
12 | ///
13 | OutboxCommand Serialize(object command);
14 |
15 | ///
16 | /// Deserializes the given into a copy of the original command.
17 | ///
18 | object Deserialize(OutboxCommand outboxCommand);
19 | }
--------------------------------------------------------------------------------
/Freakout/IFreakoutContextAccessor.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace Freakout;
4 |
5 | ///
6 | /// Accessor that enables getting the current ambient Freakout context (or NULL if there is none)
7 | ///
8 | public interface IFreakoutContextAccessor
9 | {
10 | ///
11 | /// Returns the current ambient Freakout context of type .
12 | /// If is TRUE, an is thrown if no context could be found.
13 | /// If is FALSE and there's no context, NULL is returned.
14 | /// Throws if there is a context but it is not of type .
15 | ///
16 | TContext GetContext(bool throwIfNull = true) where TContext : class, IFreakoutContext;
17 | }
--------------------------------------------------------------------------------
/Freakout/Serialization/HeaderSerializer.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Text.Json;
3 |
4 | namespace Freakout.Serialization;
5 |
6 | ///
7 | /// Built-in header serializer. This is just how store command headers are serialized. Uses System.Text.Json internally.
8 | ///
9 | public static class HeaderSerializer
10 | {
11 | ///
12 | /// Serializes the to a string.
13 | ///
14 | public static string SerializeToString(Dictionary headers) => JsonSerializer.Serialize(headers);
15 |
16 | ///
17 | /// Deserializes the string back to a Dictionary<string, string>
18 | ///
19 | public static Dictionary DeserializeFromString(string headers) => JsonSerializer.Deserialize>(headers);
20 | }
--------------------------------------------------------------------------------
/scripts/push.cmd:
--------------------------------------------------------------------------------
1 | @echo off
2 |
3 | set version=%1
4 |
5 | if "%version%"=="" (
6 | echo Please remember to specify which version to push as an argument.
7 | goto exit_fail
8 | )
9 |
10 | set reporoot=%~dp0\..
11 | set destination=%reporoot%\deploy
12 |
13 | if not exist "%destination%" (
14 | echo Could not find %destination%
15 | echo.
16 | echo Did you remember to build the packages before running this script?
17 | )
18 |
19 | set nuget=%reporoot%\tools\NuGet\NuGet.exe
20 |
21 | if not exist "%nuget%" (
22 | echo Could not find NuGet here:
23 | echo.
24 | echo "%nuget%"
25 | echo.
26 | goto exit_fail
27 | )
28 |
29 |
30 | "%nuget%" push "%destination%\*.%version%.nupkg" -Source https://nuget.org
31 | if %ERRORLEVEL% neq 0 (
32 | echo NuGet push failed.
33 | goto exit_fail
34 | )
35 |
36 |
37 |
38 |
39 |
40 |
41 | goto exit_success
42 | :exit_fail
43 | exit /b 1
44 | :exit_success
45 |
--------------------------------------------------------------------------------
/Freakout.NpgSql.Tests/Freakout.NpgSql.Tests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net9.0
5 | enable
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/Freakout/Internals/AsyncLocalFreakoutContextAccessor.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading;
3 |
4 | namespace Freakout.Internals;
5 |
6 | class AsyncLocalFreakoutContextAccessor : IFreakoutContextAccessor
7 | {
8 | internal static readonly AsyncLocal Instance = new();
9 |
10 | public TContext GetContext(bool throwIfNull = true) where TContext : class, IFreakoutContext
11 | {
12 | var instance = Instance.Value;
13 |
14 | if (instance == null)
15 | {
16 | if (!throwIfNull) return null;
17 |
18 | throw new InvalidOperationException("Could not get ambient Frekout context. Please be sure that a suitable ambient context is available by using FreakoutContextScope");
19 | }
20 |
21 | if (instance is TContext context) return context;
22 |
23 | throw new InvalidCastException(
24 | $"Ambient Freakout context of type {instance.GetType()} cannot be cast to {typeof(TContext)}");
25 | }
26 | }
--------------------------------------------------------------------------------
/Freakout.MsSql.Tests/Freakout.MsSql.Tests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net9.0
5 | enable
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/Freakout.Tests/Freakout.Tests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net9.0
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Rebus
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Freakout.Testing/Freakout.Testing.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netstandard2.0;net462;net8.0;net9.0
5 | 13
6 | mookid8000
7 | Rebus FM ApS
8 | README.md
9 | https://github.com/rebus-org/Freakout
10 | MIT
11 | True
12 | snupkg
13 | transparent_2024-04-11T18-52-17_cropped.png
14 | True
15 |
16 |
17 |
18 |
19 | True
20 | \
21 |
22 |
23 | True
24 | \
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/Freakout.MsSql/MsSqlFreakoutContext.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Data.Common;
3 |
4 | namespace Freakout.MsSql;
5 |
6 | ///
7 | /// Implementation of that wraps a
8 | /// and a
9 | ///
10 | public class MsSqlFreakoutContext : IFreakoutContext
11 | {
12 | ///
13 | /// Gets the current
14 | ///
15 | public DbConnection Connection { get; }
16 |
17 | ///
18 | /// Gets the current
19 | ///
20 | public DbTransaction Transaction { get; }
21 |
22 | ///
23 | /// Creates the context and sets it up to pass the given and
24 | /// to the implementation
25 | ///
26 | public MsSqlFreakoutContext(DbConnection connection, DbTransaction transaction)
27 | {
28 | Connection = connection ?? throw new ArgumentNullException(nameof(connection));
29 | Transaction = transaction ?? throw new ArgumentNullException(nameof(transaction));
30 | }
31 | }
--------------------------------------------------------------------------------
/Freakout.Tests/Contracts/AbstractFreakoutSystemFactory.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Microsoft.Extensions.DependencyInjection;
3 | using Microsoft.Extensions.Logging;
4 | using Nito.Disposables;
5 |
6 | namespace Freakout.Tests.Contracts;
7 |
8 | public abstract class AbstractFreakoutSystemFactory : IFreakoutSystemFactory
9 | {
10 | protected readonly CollectionDisposable disposables = new();
11 |
12 | public FreakoutSystem Create(Action before = null, Action after = null)
13 | {
14 | var services = new ServiceCollection();
15 |
16 | before?.Invoke(services);
17 |
18 | ConfigureServices(services);
19 |
20 | after?.Invoke(services);
21 |
22 | services.AddLogging(builder => builder.AddConsole());
23 |
24 | var provider = services.BuildServiceProvider();
25 |
26 | disposables.Add(provider);
27 |
28 | return GetFreakoutSystem(provider);
29 | }
30 |
31 | protected abstract FreakoutSystem GetFreakoutSystem(ServiceProvider provider);
32 |
33 | protected abstract void ConfigureServices(IServiceCollection services);
34 |
35 | public void Dispose() => disposables.Dispose();
36 | }
--------------------------------------------------------------------------------
/Freakout/Internals/InternalExtensions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using Microsoft.Extensions.DependencyInjection;
5 | using Microsoft.Extensions.Logging;
6 | using Microsoft.Extensions.Logging.Abstractions;
7 |
8 | namespace Freakout.Internals;
9 |
10 | static class InternalExtensions
11 | {
12 | public static ILogger GetLoggerFor(this IServiceProvider serviceProvider) => serviceProvider.GetLoggerFactory().CreateLogger();
13 |
14 | public static ILoggerFactory GetLoggerFactory(this IServiceProvider serviceProvider) => serviceProvider.GetService() ?? new NullLoggerFactory();
15 |
16 | public static string GetValueOrThrow(this Dictionary dictionary, string key)
17 | {
18 | if (dictionary == null) throw new ArgumentNullException(nameof(dictionary));
19 | if (key == null) throw new ArgumentNullException(nameof(key));
20 |
21 | return dictionary.TryGetValue(key, out var result)
22 | ? result
23 | : throw new KeyNotFoundException($"Could not find element with key '{key}' among these: {string.Join(", ", dictionary.Keys.Select(k => $"'{k}'"))}");
24 | }
25 | }
--------------------------------------------------------------------------------
/Freakout.NpgSql/NpgsqlFreakoutContext.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Data.Common;
3 | using Npgsql;
4 |
5 | namespace Freakout.NpgSql;
6 |
7 | ///
8 | /// Implementation of that wraps a
9 | /// and a
10 | ///
11 | public class NpgsqlFreakoutContext : IFreakoutContext
12 | {
13 | ///
14 | /// Gets the current
15 | ///
16 | public NpgsqlConnection Connection { get; }
17 |
18 | ///
19 | /// Gets the current
20 | ///
21 | public NpgsqlTransaction Transaction { get; }
22 |
23 | ///
24 | /// Creates the context and sets it up to pass the given and
25 | /// to the implementation
26 | ///
27 | public NpgsqlFreakoutContext(NpgsqlConnection connection, NpgsqlTransaction transaction)
28 | {
29 | Connection = connection ?? throw new ArgumentNullException(nameof(connection));
30 | Transaction = transaction ?? throw new ArgumentNullException(nameof(transaction));
31 | }
32 | }
--------------------------------------------------------------------------------
/Freakout.Marten/Freakout.Marten.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0;net9.0
5 | 13
6 | mookid8000
7 | Rebus FM ApS
8 | README.md
9 | https://github.com/rebus-org/Freakout
10 | MIT
11 | True
12 | snupkg
13 | transparent_2024-04-11T18-52-17_cropped.png
14 | True
15 |
16 |
17 |
18 |
19 | True
20 | \
21 |
22 |
23 | True
24 | \
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/Freakout.NpgSql/Freakout.NpgSql.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0;net9.0
5 | 13
6 | mookid8000
7 | Rebus FM ApS
8 | README.md
9 | https://github.com/rebus-org/Freakout
10 | MIT
11 | True
12 | snupkg
13 | transparent_2024-04-11T18-52-17_cropped.png
14 | True
15 |
16 |
17 |
18 |
19 | True
20 | \
21 |
22 |
23 | True
24 | \
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/Freakout/Internals/DefaultBatchDispatcher.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Diagnostics;
3 | using System.Threading;
4 | using System.Threading.Tasks;
5 | using Microsoft.Extensions.Logging;
6 |
7 | namespace Freakout.Internals;
8 |
9 | class DefaultBatchDispatcher(ICommandDispatcher commandDispatcher, ILogger logger) : IBatchDispatcher
10 | {
11 | public async Task ExecuteAsync(OutboxCommandBatch batch, CancellationToken cancellationToken = default)
12 | {
13 | foreach (var command in batch)
14 | {
15 | var stopwatch = Stopwatch.StartNew();
16 |
17 | logger.LogDebug("Executing store command {command}", command);
18 |
19 | try
20 | {
21 | await commandDispatcher.ExecuteAsync(command, cancellationToken);
22 |
23 | command.SetState(new SuccessfullyExecutedCommandState(stopwatch.Elapsed));
24 |
25 | logger.LogDebug("Successfully executed store command {command}", command);
26 | }
27 | catch (Exception exception)
28 | {
29 | command.SetState(new FailedCommandState(stopwatch.Elapsed, exception));
30 |
31 | throw new ApplicationException($"Could not execute command {command}", exception);
32 | }
33 | }
34 | }
35 | }
--------------------------------------------------------------------------------
/Freakout.MsSql/Freakout.MsSql.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net462;net8.0;net9.0
5 | 13
6 | mookid8000
7 | Rebus FM ApS
8 | README.md
9 | https://github.com/rebus-org/Freakout
10 | MIT
11 | True
12 | snupkg
13 | transparent_2024-04-11T18-52-17_cropped.png
14 | True
15 |
16 |
17 |
18 |
19 | True
20 | \
21 |
22 |
23 | True
24 | \
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/Freakout.Testing/Internals/InMemOutboxDecorator.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Runtime.Serialization;
4 | using System.Threading;
5 | using System.Threading.Tasks;
6 |
7 | namespace Freakout.Testing.Internals;
8 |
9 | class InMemOutboxDecorator(ICommandSerializer serializer, IOutbox outbox) : IOutbox
10 | {
11 | public void AddOutboxCommand(object command, Dictionary headers = null, CancellationToken cancellationToken = default)
12 | {
13 | CheckSerialization(command);
14 | outbox.AddOutboxCommand(command, headers, cancellationToken);
15 | }
16 |
17 | public async Task AddOutboxCommandAsync(object command, Dictionary headers = null, CancellationToken cancellationToken = default)
18 | {
19 | CheckSerialization(command);
20 | await outbox.AddOutboxCommandAsync(command, headers, cancellationToken);
21 | }
22 |
23 | void CheckSerialization(object command)
24 | {
25 | try
26 | {
27 | var outboxCommand = serializer.Serialize(command);
28 | var roundtrippedCommand = serializer.Deserialize(outboxCommand);
29 | }
30 | catch (Exception exception)
31 | {
32 | throw new SerializationException(
33 | $"Serialization check failed – the command {command} could not be roundtripped by serializer {serializer.GetType().Name}", exception);
34 | }
35 | }
36 | }
--------------------------------------------------------------------------------
/Freakout.Tests/Contracts/FreakoutSystem.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading;
3 | using System.Threading.Tasks;
4 | using Microsoft.Extensions.DependencyInjection;
5 |
6 | namespace Freakout.Tests.Contracts;
7 |
8 | public class FreakoutSystem(ServiceProvider ServiceProvider, Func contextFactory, Action commitAction, Action disposeAction)
9 | {
10 | public IOutbox Outbox => ServiceProvider.GetRequiredService();
11 |
12 | public IOutboxCommandStore OutboxCommandStore => ServiceProvider.GetRequiredService();
13 |
14 | public ICommandSerializer CommandSerializer => ServiceProvider.GetRequiredService();
15 |
16 | public FreakoutTestScope CreateScope() => new(contextFactory(), commitAction, disposeAction);
17 |
18 | public class FreakoutTestScope(IFreakoutContext freakoutContext, Action commitAction, Action disposeAction) : IDisposable
19 | {
20 | readonly FreakoutContextScope _innerScope = new(freakoutContext);
21 |
22 | public void Complete() => commitAction(freakoutContext);
23 |
24 | public void Dispose()
25 | {
26 | _innerScope.Dispose();
27 | disposeAction(freakoutContext);
28 | }
29 | }
30 |
31 | public Task StartCommandProcessorAsync(CancellationToken stoppingToken)
32 | {
33 | return ServiceProvider.RunBackgroundWorkersAsync(stoppingToken);
34 | }
35 | }
--------------------------------------------------------------------------------
/scripts/release.cmd:
--------------------------------------------------------------------------------
1 | @echo off
2 |
3 | set scriptsdir=%~dp0
4 | set root=%scriptsdir%\..
5 | set deploydir=%root%\deploy
6 | set project=%1
7 | set version=%2
8 |
9 | if "%project%"=="" (
10 | echo Please invoke the build script with a project name as its first argument.
11 | echo.
12 | goto exit_fail
13 | )
14 |
15 | if "%version%"=="" (
16 | echo Please invoke the build script with a version as its second argument.
17 | echo.
18 | goto exit_fail
19 | )
20 |
21 | set Version=%version%
22 |
23 | if exist "%deploydir%" (
24 | rd "%deploydir%" /s/q
25 | )
26 |
27 | pushd %root%
28 |
29 | dotnet restore --interactive
30 | if %ERRORLEVEL% neq 0 (
31 | popd
32 | goto exit_fail
33 | )
34 |
35 | dotnet pack Freakout -c Release -o "%deploydir%" -p:PackageVersion=%version% --no-restore
36 | if %ERRORLEVEL% neq 0 (
37 | popd
38 | goto exit_fail
39 | )
40 |
41 | dotnet pack Freakout.MsSql -c Release -o "%deploydir%" -p:PackageVersion=%version% --no-restore
42 | if %ERRORLEVEL% neq 0 (
43 | popd
44 | goto exit_fail
45 | )
46 |
47 | dotnet pack Freakout.NpgSql -c Release -o "%deploydir%" -p:PackageVersion=%version% --no-restore
48 | if %ERRORLEVEL% neq 0 (
49 | popd
50 | goto exit_fail
51 | )
52 |
53 | dotnet pack Freakout.Testing -c Release -o "%deploydir%" -p:PackageVersion=%version% --no-restore
54 | if %ERRORLEVEL% neq 0 (
55 | popd
56 | goto exit_fail
57 | )
58 |
59 | call scripts\push.cmd "%version%"
60 |
61 | popd
62 |
63 |
64 |
65 |
66 |
67 |
68 | goto exit_success
69 | :exit_fail
70 | exit /b 1
71 | :exit_success
--------------------------------------------------------------------------------
/Freakout.Tests/TestExtensions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Concurrent;
3 | using System.Threading;
4 | using System.Threading.Tasks;
5 | using Microsoft.Extensions.DependencyInjection;
6 | using Microsoft.Extensions.Hosting;
7 |
8 | namespace Freakout.Tests;
9 |
10 | public static class TestExtensions
11 | {
12 | public static async Task RunBackgroundWorkersAsync(this ServiceProvider serviceProvider, CancellationToken stoppingToken)
13 | {
14 | ArgumentNullException.ThrowIfNull(serviceProvider);
15 |
16 | var servicesToStop = new ConcurrentStack();
17 |
18 | try
19 | {
20 | var hostedServices = serviceProvider.GetServices();
21 |
22 | foreach (var service in hostedServices)
23 | {
24 | await service.StartAsync(stoppingToken);
25 | servicesToStop.Push(service);
26 | }
27 |
28 | await Task.Delay(-1, stoppingToken);
29 | }
30 | catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
31 | {
32 | // it's ok
33 | }
34 | catch (Exception exception)
35 | {
36 | Console.WriteLine($"Failed to run background services: {exception}");
37 | }
38 | finally
39 | {
40 | while (servicesToStop.TryPop(out var service))
41 | {
42 | await service.StopAsync(CancellationToken.None);
43 | }
44 | }
45 | }
46 | }
--------------------------------------------------------------------------------
/Freakout.Testing/Internals/InMemOutbox.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Concurrent;
3 | using System.Collections.Generic;
4 | using System.Threading;
5 | using System.Threading.Tasks;
6 | #pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
7 |
8 | namespace Freakout.Testing.Internals;
9 |
10 | class InMemOutbox(IFreakoutContextAccessor freakoutContextAccessor, ConcurrentQueue commands) : IOutbox
11 | {
12 | public event Action CommandAddedToQueue;
13 |
14 | public void AddOutboxCommand(object command, Dictionary headers = null, CancellationToken cancellationToken = default)
15 | {
16 | Enqueue(command, headers);
17 | }
18 |
19 | public async Task AddOutboxCommandAsync(object command, Dictionary headers = null, CancellationToken cancellationToken = default)
20 | {
21 | Enqueue(command, headers);
22 | }
23 |
24 | void Enqueue(object command, Dictionary headers)
25 | {
26 | var context = freakoutContextAccessor.GetContext();
27 | var inMemOutboxCommand = new InMemOutboxCommand(headers ?? new(), command);
28 |
29 | context.UnmountedCallback ??= enlistedCommands =>
30 | {
31 | foreach (var cmd in enlistedCommands)
32 | {
33 | commands.Enqueue(cmd);
34 | CommandAddedToQueue?.Invoke(cmd);
35 | }
36 | };
37 |
38 | context.Enlist(inMemOutboxCommand);
39 | }
40 | }
--------------------------------------------------------------------------------
/Freakout/Freakout.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netstandard2.0;net462;net8.0;net9.0
5 | 13
6 | mookid8000
7 | Rebus FM ApS
8 | README.md
9 | https://github.com/rebus-org/Freakout
10 | MIT
11 | True
12 | snupkg
13 | transparent_2024-04-11T18-52-17_cropped.png
14 | True
15 |
16 |
17 |
18 |
19 | True
20 | \
21 |
22 |
23 | True
24 | \
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/Freakout.NpgSql.Tests/NpgSqlTestHelper.cs:
--------------------------------------------------------------------------------
1 | using Nito.AsyncEx.Synchronous;
2 | using Nito.Disposables;
3 | using Npgsql;
4 | using NUnit.Framework;
5 | using Testcontainers.PostgreSql;
6 |
7 | namespace Freakout.NpgSql.Tests;
8 |
9 | [SetUpFixture]
10 | class NpgsqlTestHelper
11 | {
12 | static readonly CollectionDisposable Disposables = new();
13 |
14 | static readonly Lazy LazyConnectionString = new(() =>
15 | {
16 | var connectionString = Environment.GetEnvironmentVariable("NPGSQL_TEST_CONNECTIONSTRING");
17 | if (!string.IsNullOrWhiteSpace(connectionString)) return connectionString;
18 |
19 | var builder = new PostgreSqlBuilder();
20 | var container = builder.Build();
21 |
22 | container.StartAsync().WaitAndUnwrapException();
23 |
24 | Disposables.Add(new Disposable(() => container.DisposeAsync()));
25 |
26 | return container.GetConnectionString();
27 | });
28 |
29 | public static string ConnectionString => LazyConnectionString.Value;
30 |
31 | [OneTimeTearDown]
32 | public void CleanUp() => Disposables.Dispose();
33 |
34 | public static void DropTable(string tableName) => DropTable("dbo", tableName);
35 |
36 | public static void DropTable(string schemaName, string tableName)
37 | {
38 | using var connection = new NpgsqlConnection(ConnectionString);
39 | connection.Open();
40 |
41 | using var command = connection.CreateCommand();
42 | command.CommandText = $@"DROP TABLE IF EXISTS ""{schemaName}"".""{tableName}"";";
43 | command.ExecuteNonQuery();
44 | }
45 |
46 | }
--------------------------------------------------------------------------------
/Freakout/PendingOutboxCommand.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 |
4 | namespace Freakout;
5 |
6 | ///
7 | /// Raw store command after having been persisted
8 | ///
9 | public record PendingOutboxCommand(Guid Id, DateTimeOffset Created, Dictionary Headers, byte[] Payload)
10 | : OutboxCommand(Headers, Payload)
11 | {
12 | ///
13 | /// Gets the command state
14 | ///
15 | public CommandState State { get; private set; } = new PendingCommandState();
16 |
17 | ///
18 | /// Sets to
19 | ///
20 | public void SetState(CommandState state) => State = state ?? throw new ArgumentNullException(nameof(state));
21 | }
22 |
23 | ///
24 | /// Abstract command state. Represents a state that a command can be in.
25 | ///
26 | public abstract record CommandState;
27 |
28 | ///
29 | /// Represents the state of the command when it has been fetched from the command store.
30 | ///
31 | public record PendingCommandState : CommandState;
32 |
33 | ///
34 | /// Represents the state of the command when it has been successfully executed and executing it took
35 | ///
36 | public record SuccessfullyExecutedCommandState(TimeSpan Elapsed) : CommandState;
37 |
38 | ///
39 | /// Represents the state of the command when executing it failed with and took
40 | ///
41 | public record FailedCommandState(TimeSpan Elapsed, Exception Exception) : CommandState;
--------------------------------------------------------------------------------
/Freakout.MsSql/MsSqlFreakoutConfiguration.cs:
--------------------------------------------------------------------------------
1 | using Freakout.MsSql.Internals;
2 | using Microsoft.Extensions.DependencyInjection;
3 |
4 | namespace Freakout.MsSql;
5 |
6 | ///
7 | /// Freakout configuration for using Microsoft SQL Server as the store.
8 | ///
9 | /// Configures the connection string to use to connect to SQL Server
10 | public class MsSqlFreakoutConfiguration(string connectionString) : FreakoutConfiguration
11 | {
12 | ///
13 | /// Configures the store table schema name. Defaults to "dbo".
14 | ///
15 | public string SchemaName { get; set; } = "dbo";
16 |
17 | ///
18 | /// Configures the store table name. Defaults to "OutboxCommands".
19 | ///
20 | public string TableName { get; set; } = "OutboxCommands";
21 |
22 | ///
23 | /// Configures whether the schema should be created automatically
24 | ///
25 | public bool AutomaticallyCreateSchema { get; set; } = true;
26 |
27 | ///
28 | protected override void ConfigureServices(IServiceCollection services)
29 | {
30 | services.AddSingleton(_ =>
31 | {
32 | var commandStore = new MsSqlOutboxCommandStore(connectionString, TableName, SchemaName);
33 |
34 | if (AutomaticallyCreateSchema)
35 | {
36 | commandStore.CreateSchema();
37 | }
38 |
39 | return commandStore;
40 | });
41 |
42 | services.AddScoped(p => new MsSqlOutbox(this, p.GetRequiredService()));
43 | }
44 | }
--------------------------------------------------------------------------------
/Freakout.NpgSql/NpgSqlFreakoutConfiguration.cs:
--------------------------------------------------------------------------------
1 | using Freakout.NpgSql.Internals;
2 | using Microsoft.Extensions.DependencyInjection;
3 |
4 | namespace Freakout.NpgSql;
5 |
6 | ///
7 | /// Freakout configuration for using PostgreSQL as the store.
8 | ///
9 | /// Configures the connection string to use to connect to Postgres
10 | public class NpgsqlFreakoutConfiguration(string connectionString) : FreakoutConfiguration
11 | {
12 | ///
13 | /// Configures the store table schema name. Defaults to "public".
14 | ///
15 | public string SchemaName { get; set; } = "public";
16 |
17 | ///
18 | /// Configures the store table name. Defaults to "OutboxCommands".
19 | ///
20 | public string TableName { get; set; } = "outbox_commands";
21 |
22 | ///
23 | /// Configures whether the schema should be created automatically
24 | ///
25 | public bool AutomaticallyCreateSchema { get; set; } = true;
26 |
27 | ///
28 | protected override void ConfigureServices(IServiceCollection services)
29 | {
30 | services.AddSingleton(_ =>
31 | {
32 | var commandStore = new NpgsqlOutboxCommandStore(connectionString, TableName, SchemaName);
33 |
34 | if (AutomaticallyCreateSchema)
35 | {
36 | commandStore.CreateSchema();
37 | }
38 |
39 | return commandStore;
40 | });
41 |
42 | services.AddScoped(p => new NpgsqlOutbox(this, p.GetRequiredService()));
43 | }
44 | }
--------------------------------------------------------------------------------
/Freakout.MsSql.Tests/MsSqlTestHelper.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Data.SqlClient;
2 | using Nito.AsyncEx.Synchronous;
3 | using Nito.Disposables;
4 | using NUnit.Framework;
5 | using Testcontainers.MsSql;
6 |
7 | namespace Freakout.MsSql.Tests;
8 |
9 | [SetUpFixture]
10 | class MsSqlTestHelper
11 | {
12 | static readonly CollectionDisposable Disposables = new();
13 |
14 | static readonly Lazy LazyConnectionString = new(() =>
15 | {
16 | var connectionString = Environment.GetEnvironmentVariable("MSSQL_TEST_CONNECTIONSTRING");
17 | if (!string.IsNullOrWhiteSpace(connectionString)) return connectionString;
18 |
19 | var builder = new MsSqlBuilder();
20 | var container = builder.Build();
21 |
22 | container.StartAsync().WaitAndUnwrapException();
23 |
24 | Disposables.Add(new Disposable(() => container.DisposeAsync()));
25 |
26 | return container.GetConnectionString();
27 | });
28 |
29 | public static string ConnectionString => LazyConnectionString.Value;
30 |
31 | [OneTimeTearDown]
32 | public void CleanUp() => Disposables.Dispose();
33 |
34 | public static void DropTable(string tableName) => DropTable("dbo", tableName);
35 |
36 | public static void DropTable(string schemaName, string tableName)
37 | {
38 | using var connection = new SqlConnection(ConnectionString);
39 | connection.Open();
40 |
41 | using var command = connection.CreateCommand();
42 | command.CommandText = $@"
43 | IF EXISTS (SELECT TOP 1 * FROM sys.tables t JOIN sys.schemas s ON t.schema_id = s.schema_id WHERE t.name = '{tableName}' AND s.name = '{schemaName}')
44 | BEGIN
45 | DROP TABLE [{schemaName}].[{tableName}]
46 | END
47 | ";
48 | command.ExecuteNonQuery();
49 | }
50 |
51 | }
--------------------------------------------------------------------------------
/Freakout.MsSql.Tests/MsSqlFreakoutSystemFactory.cs:
--------------------------------------------------------------------------------
1 | using Freakout.Config;
2 | using Freakout.Tests.Contracts;
3 | using Microsoft.Data.SqlClient;
4 | using Microsoft.Extensions.DependencyInjection;
5 | using Testy.General;
6 |
7 | namespace Freakout.MsSql.Tests;
8 |
9 | public class MsSqlFreakoutSystemFactory : AbstractFreakoutSystemFactory
10 | {
11 | protected override void ConfigureServices(IServiceCollection services)
12 | {
13 | var tableName = $"outbox-{Guid.NewGuid():N}";
14 |
15 | disposables.Add(new DisposableCallback(() => MsSqlTestHelper.DropTable(tableName)));
16 |
17 | var configuration = new MsSqlFreakoutConfiguration(MsSqlTestHelper.ConnectionString)
18 | {
19 | TableName = tableName,
20 | OutboxPollInterval = TimeSpan.FromSeconds(1)
21 | };
22 |
23 | services.AddFreakout(configuration);
24 | }
25 |
26 | protected override FreakoutSystem GetFreakoutSystem(ServiceProvider provider)
27 | {
28 | IFreakoutContext ContextFactory()
29 | {
30 | var connection = new SqlConnection(MsSqlTestHelper.ConnectionString);
31 | connection.Open();
32 | var transaction = connection.BeginTransaction();
33 | return new MsSqlFreakoutContext(connection, transaction);
34 | }
35 |
36 | void CommitAction(IFreakoutContext context)
37 | {
38 | var ctx = (MsSqlFreakoutContext)context;
39 | ctx.Transaction.Commit();
40 | }
41 |
42 | void DisposeAction(IFreakoutContext context)
43 | {
44 | var ctx = (MsSqlFreakoutContext)context;
45 | ctx.Transaction.Dispose();
46 | ctx.Connection.Dispose();
47 | }
48 |
49 | return new(provider, ContextFactory, CommitAction, DisposeAction);
50 | }
51 | }
--------------------------------------------------------------------------------
/Freakout.NpgSql/Internals/NpgSqlOutbox.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Threading;
4 | using System.Threading.Tasks;
5 |
6 | // ReSharper disable MethodHasAsyncOverloadWithCancellation
7 |
8 | namespace Freakout.NpgSql.Internals;
9 |
10 | class NpgsqlOutbox(NpgsqlFreakoutConfiguration configuration, IFreakoutContextAccessor freakoutContextAccessor) : IOutbox
11 | {
12 | public void AddOutboxCommand(object command, Dictionary headers = null, CancellationToken cancellationToken = default)
13 | {
14 | if (command == null) throw new ArgumentNullException(nameof(command));
15 |
16 | var context = freakoutContextAccessor.GetContext();
17 | var transaction = context.Transaction;
18 |
19 | transaction.AddOutboxCommand(
20 | schemaName: configuration.SchemaName,
21 | tableName: configuration.TableName,
22 | serializer: configuration.CommandSerializer,
23 | command: command,
24 | headers: headers
25 | );
26 | }
27 |
28 | public async Task AddOutboxCommandAsync(object command, Dictionary headers = null, CancellationToken cancellationToken = default)
29 | {
30 | if (command == null) throw new ArgumentNullException(nameof(command));
31 |
32 | var context = freakoutContextAccessor.GetContext();
33 | var transaction = context.Transaction;
34 |
35 | await transaction.AddOutboxCommandAsync(
36 | schemaName: configuration.SchemaName,
37 | tableName: configuration.TableName,
38 | serializer: configuration.CommandSerializer,
39 | command: command,
40 | headers: headers,
41 | cancellationToken: cancellationToken
42 | );
43 | }
44 | }
--------------------------------------------------------------------------------
/Freakout/Internals/CommandDispatcher.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Concurrent;
3 | using System.Threading;
4 | using System.Threading.Tasks;
5 | using Microsoft.Extensions.DependencyInjection;
6 |
7 | namespace Freakout.Internals;
8 |
9 | abstract class CommandDispatcher(ICommandSerializer commandSerializer, IServiceScopeFactory serviceScopeFactory) : ICommandDispatcher
10 | {
11 | readonly ConcurrentDictionary> _invokers = new();
12 |
13 | public async Task ExecuteAsync(OutboxCommand outboxCommand, CancellationToken cancellationToken = default)
14 | {
15 | var command = commandSerializer.Deserialize(outboxCommand);
16 | var type = command.GetType();
17 |
18 | var invoker = _invokers.GetOrAdd(type, CreateInvoker);
19 |
20 | await invoker(command, cancellationToken);
21 | }
22 |
23 | protected abstract Func