().Database.ProviderName;
16 | }
17 | }
18 |
19 |
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/documentation/docs/samples/distributed-sample.md:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar_position: 2
3 | ---
4 |
5 | # Distributed Sample
6 | Distributed sample consists of four modules with two instances each. Example shows a simplified banking domain
7 | :::info
8 | Docker is required.
9 | :::
10 |
11 | Clone repo and open `TransactionalBox.sln` via Visual Studio. Set the `Bank` as startup and then run.
12 |
13 | Run browser and open below links:
14 | **CustomerRegistrations:** `https://localhost:1000/swagger/index.html`
15 | **Customers**: `https://localhost:2000/swagger/index.html`
16 | **BankAccounts:** `https://localhost:3000/swagger/index.html`
17 | **Loans:** `https://localhost:4000/swagger/index.html`
18 |
19 |
20 | 
21 |
22 |
23 |
24 | Don't forget to use break points to learn.
25 | Have fun :smiley:!
--------------------------------------------------------------------------------
/source/TransactionalBox/Internals/Outbox/Hooks/Handlers/AddMessagesToTransport/Logger/AddMessagesToTransportLogger.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.Logging;
2 |
3 | namespace TransactionalBox.Internals.Outbox.Hooks.Handlers.AddMessagesToTransport.Logger
4 | {
5 | internal sealed partial class AddMessagesToTransportLogger : IAddMessagesToTransportLogger
6 | {
7 | private readonly ILogger _logger;
8 |
9 | public AddMessagesToTransportLogger(ILogger logger) => _logger = logger;
10 |
11 | [LoggerMessage(0, LogLevel.Information, "{eventHookHandlerName} '{hookId}' (Iteration: {iteration} NumberOfMessages: {numberOfMessages} MaxBatchSize: {maxbatchSize})", SkipEnabledCheck = true)]
12 | public partial void Added(string eventHookHandlerName, Guid hookId, long iteration, int numberOfMessages, int maxBatchSize);
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/source/TransactionalBox.EntityFrameworkCore/Internals/Inbox/EntityTypeConfigurations/InboxMessageEntityTypeConfiguration.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore.Metadata.Builders;
2 | using Microsoft.EntityFrameworkCore;
3 | using TransactionalBox.Internals.Inbox.Storage;
4 |
5 | namespace TransactionalBox.EntityFrameworkCore.Internals.Inbox.EntityTypeConfigurations
6 | {
7 | internal sealed class InboxMessageEntityTypeConfiguration : IEntityTypeConfiguration
8 | {
9 | public void Configure(EntityTypeBuilder builder)
10 | {
11 | builder.HasKey(x => x.Id);
12 | builder.Property(x => x.OccurredUtc);
13 | builder.Property(x => x.IsProcessed).IsConcurrencyToken();
14 | builder.Property(x => x.Topic);
15 | builder.Property(x => x.Payload);
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/samples/Bank/Bank.dcproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 2.1
5 | Linux
6 | False
7 | af9aebe9-ceb7-490a-828e-2e9ba6b4400b
8 | LaunchBrowser
9 | {Scheme}://localhost:{ServicePort}/swagger
10 | transactionalbox.customerregistrations
11 |
12 |
13 |
14 | docker-compose.yml
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/source/TransactionalBox/Internals/InternalPackages/DistributedLock/ExtensionAddDistributedLock.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.DependencyInjection;
2 | using TransactionalBox.Internals.InternalPackages.KeyedInMemoryLock;
3 |
4 | namespace TransactionalBox.Internals.InternalPackages.DistributedLock
5 | {
6 | internal static class ExtensionAddDistributedLock
7 | {
8 | internal static void AddDistributedLock(
9 | this IServiceCollection services,
10 | Action storageConfiguration)
11 | where T : Lock, new()
12 | {
13 | var storage = new DistributedLockStorageConfigurator(services);
14 |
15 | storageConfiguration(storage);
16 |
17 | services.AddKeyedInMemoryLock();
18 | services.AddSingleton, InternalDistributedLock>();
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/samples/Bank/TransactionalBox.CustomerRegistrations/Database/CustomerRegistrationDbContext.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore;
2 | using TransactionalBox.CustomerRegistrations.Models;
3 |
4 | namespace TransactionalBox.CustomerRegistrations.Database
5 | {
6 | public sealed class CustomerRegistrationDbContext : DbContext
7 | {
8 | public DbSet CustomerRegistrations { get; set; }
9 |
10 | public CustomerRegistrationDbContext() : base() { }
11 |
12 | public CustomerRegistrationDbContext(DbContextOptions options)
13 | : base(options) { }
14 |
15 | protected override void OnModelCreating(ModelBuilder modelBuilder)
16 | {
17 | modelBuilder.AddOutbox();
18 | modelBuilder.ApplyConfigurationsFromAssembly(typeof(CustomerRegistrationDbContext).Assembly);
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/samples/Bank/TransactionalBox.CustomerRegistrations/Database/CustomerRegistrationEntityTypeConfiguration.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore;
2 | using Microsoft.EntityFrameworkCore.Metadata.Builders;
3 | using TransactionalBox.CustomerRegistrations.Models;
4 |
5 | namespace TransactionalBox.CustomerRegistrations.Database
6 | {
7 | public class CustomerRegistrationEntityTypeConfiguration : IEntityTypeConfiguration
8 | {
9 | public void Configure(EntityTypeBuilder builder)
10 | {
11 | builder.HasKey(x => x.Id);
12 | builder.Property(x => x.FirstName);
13 | builder.Property(x => x.LastName);
14 | builder.Property(x => x.Age);
15 | builder.Property(x => x.IsApproved);
16 | builder.Property(x => x.CreatedAtUtc);
17 | builder.Property(x => x.UpdatedAtUtc);
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/source/TransactionalBox/Internals/InternalPackages/EventHooks/EventHookPublisher.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.DependencyInjection;
2 |
3 | namespace TransactionalBox.Internals.InternalPackages.EventHooks
4 | {
5 | internal sealed class EventHookPublisher : IEventHookPublisher
6 | {
7 | private readonly IServiceProvider _serviceProvider;
8 |
9 | public EventHookPublisher(IServiceProvider serviceProvider)
10 | {
11 | _serviceProvider = serviceProvider;
12 | }
13 |
14 | public async Task PublishAsync()
15 | where TEventHook : EventHook, new()
16 | {
17 | var eventHookHub = _serviceProvider.GetService>();
18 |
19 | if (eventHookHub is not null)
20 | {
21 | await eventHookHub.PublishAsync().AsTask().ConfigureAwait(false);
22 | }
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/architecture-decision-records/2024-06.1-change-structure-of-project.md:
--------------------------------------------------------------------------------
1 | # Title
2 | Change structure of project.
3 |
4 | ## Description
5 | The current project architecture consists of many packages, this can be overwhelming for new users.
6 |
7 |
8 |

9 |
10 |
11 | The main components `TransactionalBox.Inbox` and `TransactionalBox.Outbox` should be moved to the main package `TransactionalBox`. When using the package via extension methods, the user will decide which components to register in container.
12 |
13 | Packages with external dependencies are also worth merging. e.g. `TransactionalBox.Outbox.Kafka` and `TransactionalBox.Inbox.Kafka` should be moved to the `TransactionalBox.Kafka` package.
14 |
15 | Internal packages from the `Internals` folder should be moved to the `TransactionalBox` package.
16 |
17 | In this way we can go from 7 to 3 public packages.
--------------------------------------------------------------------------------
/documentation/sidebars.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Creating a sidebar enables you to:
3 | - create an ordered group of docs
4 | - render a sidebar for each doc of that group
5 | - provide next/previous navigation
6 |
7 | The sidebars can be generated from the filesystem, or explicitly defined here.
8 |
9 | Create as many sidebars as you want.
10 | */
11 |
12 | // @ts-check
13 |
14 | /** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */
15 | const sidebars = {
16 | // By default, Docusaurus generates a sidebar from the docs folder structure
17 | tutorialSidebar: [{type: 'autogenerated', dirName: '.'}],
18 |
19 | // But you can create a sidebar manually
20 | /*
21 | tutorialSidebar: [
22 | 'intro',
23 | 'hello',
24 | {
25 | type: 'category',
26 | label: 'Tutorial',
27 | items: ['tutorial-basics/create-a-document'],
28 | },
29 | ],
30 | */
31 | };
32 |
33 | export default sidebars;
34 |
--------------------------------------------------------------------------------
/source/TransactionalBox/Internals/InternalPackages/EventHooks/EventHooksStartup.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.Hosting;
2 |
3 | namespace TransactionalBox.Internals.InternalPackages.EventHooks
4 | {
5 | internal sealed class EventHooksStartup : BackgroundService
6 | {
7 | private readonly IEnumerable _hookListenersLauncher;
8 |
9 | public EventHooksStartup(
10 | IEnumerable hookListenersLauncher)
11 | {
12 | _hookListenersLauncher = hookListenersLauncher;
13 | }
14 |
15 | protected override Task ExecuteAsync(CancellationToken stoppingToken)
16 | {
17 | foreach (var hookListenersLauncher in _hookListenersLauncher)
18 | {
19 | hookListenersLauncher.LaunchAsync(stoppingToken);
20 | }
21 |
22 | return Task.CompletedTask;
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/source/TransactionalBox/Internals/TransactionalBoxBuilder.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.Configuration;
2 | using Microsoft.Extensions.DependencyInjection;
3 | using TransactionalBox.Builders;
4 |
5 | namespace TransactionalBox.Internals
6 | {
7 | internal sealed class TransactionalBoxBuilder : ITransactionalBoxBuilder
8 | {
9 | public IServiceCollection Services { get; }
10 |
11 | public IConfiguration Configuration { get; }
12 |
13 | internal TransactionalBoxBuilder(
14 | IServiceCollection services,
15 | IConfiguration configuration)
16 | {
17 | Services = services;
18 |
19 | if (configuration is not null)
20 | {
21 | Configuration = configuration;
22 | }
23 | else
24 | {
25 | Configuration = new ConfigurationBuilder().Build();
26 | }
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/documentation/README.md:
--------------------------------------------------------------------------------
1 | # Website
2 |
3 | This website is built using [Docusaurus](https://docusaurus.io/), a modern static website generator.
4 |
5 | ### Installation
6 |
7 | ```
8 | $ yarn
9 | ```
10 |
11 | ### Local Development
12 |
13 | ```
14 | $ yarn start
15 | ```
16 |
17 | This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server.
18 |
19 | ### Build
20 |
21 | ```
22 | $ yarn build
23 | ```
24 |
25 | This command generates static content into the `build` directory and can be served using any static contents hosting service.
26 |
27 | ### Deployment
28 |
29 | Using SSH:
30 |
31 | ```
32 | $ USE_SSH=true yarn deploy
33 | ```
34 |
35 | Not using SSH:
36 |
37 | ```
38 | $ GIT_USER= yarn deploy
39 | ```
40 |
41 | If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch.
42 |
--------------------------------------------------------------------------------
/source/TransactionalBox/Internals/Inbox/Extensions/InboxExtensionUseInMemoryTransport.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.DependencyInjection;
2 | using TransactionalBox.Internals.Inbox.Transport.ContractsToImplement;
3 | using TransactionalBox.Internals.Inbox.Transport.InMemory;
4 | using TransactionalBox.Configurators.Inbox;
5 | using TransactionalBox.Internals.InternalPackages.Transport;
6 |
7 | namespace TransactionalBox.Internals.Inbox.Extensions
8 | {
9 | internal static class InboxExtensionUseInMemoryTransport
10 | {
11 | internal static void UseInMemoryTransport(this IInboxTransportConfigurator configurator)
12 | {
13 | var services = configurator.Services;
14 |
15 | services.UseInternalInMemoryTransport();
16 | services.AddSingleton();
17 | services.AddSingleton();
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/source/TransactionalBox.Kafka/Internals/Outbox/KafkaConfigFactory.cs:
--------------------------------------------------------------------------------
1 | using Confluent.Kafka;
2 |
3 | namespace TransactionalBox.Kafka.Internals.Outbox
4 | {
5 | internal sealed class KafkaConfigFactory
6 | {
7 | private readonly IOutboxKafkaSettings _outboxWorkerKafkaSettings;
8 |
9 | private ProducerConfig? _config = null;
10 |
11 | public KafkaConfigFactory(
12 | IOutboxKafkaSettings outboxWorkerKafkaSettings)
13 | {
14 | _outboxWorkerKafkaSettings = outboxWorkerKafkaSettings;
15 | }
16 |
17 | internal ProducerConfig Create()
18 | {
19 | if (_config is not null)
20 | {
21 | return _config;
22 | }
23 |
24 | _config = new ProducerConfig()
25 | {
26 | BootstrapServers = _outboxWorkerKafkaSettings.BootstrapServers,
27 | };
28 |
29 | return _config;
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/source/TransactionalBox/Internals/Outbox/Extensions/OutboxExtensionUseInMemoryTransport.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.DependencyInjection;
2 | using TransactionalBox.Internals.Outbox.Transport.ContractsToImplement;
3 | using TransactionalBox.Internals.Outbox.Transport.InMemory;
4 | using TransactionalBox.Configurators.Outbox;
5 | using TransactionalBox.Internals.InternalPackages.Transport;
6 |
7 | namespace TransactionalBox.Internals.Outbox.Extensions
8 | {
9 | internal static class OutboxExtensionUseInMemoryTransport
10 | {
11 | internal static void UseInMemoryTransport(this IOutboxTransportConfigurator configurator)
12 | {
13 | var services = configurator.Services;
14 |
15 | services.UseInternalInMemoryTransport();
16 | services.AddSingleton();
17 | services.AddSingleton(new InMemoryTransportMessageSizeSettings());
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/.github/workflows/documentation-test-deploy.yaml:
--------------------------------------------------------------------------------
1 | name: Test documentation deployment
2 |
3 | on:
4 | pull_request:
5 | branches:
6 | - main
7 | paths:
8 | - '.github/workflows/documentation*.yaml'
9 | - 'documentation/**'
10 |
11 | jobs:
12 | deploy:
13 |
14 | name: Test documentation deployment
15 | runs-on: ubuntu-latest
16 | permissions:
17 | contents: write
18 | concurrency:
19 | group: ${{ github.workflow }}-${{ github.ref }}
20 | defaults:
21 | run:
22 | working-directory: ./documentation
23 |
24 | steps:
25 | - uses: actions/checkout@v4
26 | with:
27 | fetch-depth: 0
28 |
29 | - uses: actions/setup-node@v4
30 | with:
31 | node-version: 18
32 | cache: npm
33 | cache-dependency-path: ./documentation/package-lock.json
34 |
35 | - name: Install dependencies
36 | run: npm ci
37 |
38 | - name: Test build website
39 | run: npm run build
--------------------------------------------------------------------------------
/source/TransactionalBox.EntityFrameworkCore/Internals/Outbox/ImplementedContracts/EntityFrameworkOutboxStorage.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore;
2 | using TransactionalBox.Internals.Outbox.Storage;
3 | using TransactionalBox.Internals.Outbox.Storage.ContractsToImplement;
4 |
5 | namespace TransactionalBox.EntityFrameworkCore.Internals.Outbox.ImplementedContracts
6 | {
7 | internal sealed class EntityFrameworkOutboxStorage : IOutboxStorage
8 | {
9 | private readonly DbSet _outbox;
10 |
11 | public EntityFrameworkOutboxStorage(DbContext dbContext)
12 | {
13 | _outbox = dbContext.Set();
14 | }
15 |
16 | public Task Add(OutboxMessageStorage message)
17 | {
18 | return _outbox.AddAsync(message).AsTask();
19 | }
20 |
21 | public Task AddRange(IEnumerable messages)
22 | {
23 | return _outbox.AddRangeAsync(messages);
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/samples/Bank/TransactionalBox.Loans/Messages/CreatedCustomerEventMessageHandler.cs:
--------------------------------------------------------------------------------
1 | using TransactionalBox.Loans.Database;
2 | using TransactionalBox.Loans.Models;
3 |
4 | namespace TransactionalBox.Loans.Messages
5 | {
6 | public sealed class CreatedCustomerEventMessageHandler : IInboxHandler
7 | {
8 | private readonly LoansDbContext _loansDbContext;
9 |
10 | public CreatedCustomerEventMessageHandler(LoansDbContext loansDbContext)
11 | {
12 | _loansDbContext = loansDbContext;
13 | }
14 |
15 | public async Task Handle(CreatedCustomerEventMessage message, IExecutionContext executionContext)
16 | {
17 | var loan = new Loan()
18 | {
19 | Id = Guid.NewGuid(),
20 | CustomerId = message.Id,
21 | Amount = 0,
22 | };
23 |
24 | await _loansDbContext.Loans.AddAsync(loan);
25 | await _loansDbContext.SaveChangesAsync();
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/source/TransactionalBox.EntityFrameworkCore/Internals/Outbox/EntityTypeConfigurations/OutboxMessageEntityTypeConfiguration.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore;
2 | using Microsoft.EntityFrameworkCore.Metadata.Builders;
3 | using TransactionalBox.Internals.Outbox.Storage;
4 |
5 | namespace TransactionalBox.EntityFrameworkCore.Internals.Outbox.EntityTypeConfigurations
6 | {
7 | internal sealed class OutboxMessageEntityTypeConfiguration : IEntityTypeConfiguration
8 | {
9 | public void Configure(EntityTypeBuilder builder)
10 | {
11 | builder.HasKey(x => x.Id);
12 | builder.Property(x => x.OccurredUtc).IsRequired();
13 | builder.Property(x => x.Topic).IsRequired();
14 | builder.Property(x => x.Payload).IsRequired();
15 | builder.Property(x => x.LockUtc);
16 | builder.Property(x => x.IsProcessed).IsConcurrencyToken();
17 | builder.Property(x => x.JobId).HasMaxLength(254); //TODO
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/source/TransactionalBox/Internals/Outbox/Storage/SequentialGuidConfigurator.cs:
--------------------------------------------------------------------------------
1 | using TransactionalBox.Internals.InternalPackages.SequentialGuid;
2 | using TransactionalBox.Internals.Outbox.Storage.ContractsToImplement;
3 |
4 | namespace TransactionalBox.Internals.Outbox.Storage
5 | {
6 | internal sealed class SequentialGuidConfigurator
7 | {
8 | private readonly IStorageProvider _storageProvider;
9 |
10 | public SequentialGuidConfigurator(IStorageProvider storageProvider)
11 | {
12 | _storageProvider = storageProvider;
13 | }
14 |
15 |
16 | public SequentialGuidType Create()
17 | {
18 | var providerName = _storageProvider.ProviderName;
19 |
20 | return providerName switch
21 | {
22 | // TODO Check provider name
23 | // SequentialAtEnd SQL Server
24 | // SequentialAsBinar Oracle
25 | _ => SequentialGuidType.SequentialAsString, // MySQL, PostgreSQL
26 | };
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/source/TransactionalBox/Internals/Inbox/BackgroundProcesses/CleanUpIdempotencyKeys/Logger/CleanUpIdempotencyKeysLogger.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.Logging;
2 |
3 | namespace TransactionalBox.Internals.Inbox.BackgroundProcesses.CleanUpIdempotencyKeys.Logger
4 | {
5 | internal sealed partial class CleanUpIdempotencyKeysLogger : ICleanUpIdempotencyKeysLogger
6 | {
7 | private readonly ILogger _logger;
8 |
9 | public CleanUpIdempotencyKeysLogger(ILogger logger) => _logger = logger;
10 |
11 | [LoggerMessage(0, LogLevel.Information, "{name} '{id}' (Iteration: {iteration} NumberOfMessages: {numberOfMessages})", SkipEnabledCheck = true)]
12 | public partial void CleanedUp(string name, Guid id, long iteration, int numberOfMessages);
13 |
14 | [LoggerMessage(0, LogLevel.Error, "{name} (Attempt: {attempt} Delay: {msDelay}ms) unexpected exception", SkipEnabledCheck = true)]
15 | public partial void UnexpectedException(string name, long attempt, long msDelay, Exception exception);
16 |
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/architecture-decision-records/2024-03.1-batch-processing-of-messages.md:
--------------------------------------------------------------------------------
1 |
2 | # Title
3 | Batch processing of messages
4 |
5 | ## Description
6 | Getting of messages from storage should be processed in packages.
7 |
8 | Then these messages should be converted to transport messages (one transport message = collection of the same messages).
9 |
10 | Batch processing reduce number of call over the network to storage (database) and transport.
11 |
12 | The number of transactions performed will be lower, which is also important for performance.
13 |
14 | Batch processing is more efficient than one by one processing with an increasing number of messages.
15 |
16 | In contrast, this solution increases the complexity of the system. Deduplication of grouped messages is more difficult than one-by-one processing.
17 | Furthermore, if we have large batches then when an error occurs with a write to the storage on the outbox side, the number of duplicate messages added to the transport increases.
18 |
19 | The user must be able to set the batch size to adjust the solution to the needs of the project.
--------------------------------------------------------------------------------
/source/TransactionalBox/Settings/Inbox/InboxSettings.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.DependencyInjection;
2 | using TransactionalBox;
3 | using TransactionalBox.Configurators.Inbox;
4 | using TransactionalBox.Internals.Inbox.Configurators;
5 |
6 | namespace TransactionalBox.Settings.Inbox
7 | {
8 | public sealed class InboxSettings
9 | {
10 | public AddMessagesToInboxSettings AddMessagesToInboxSettings { get; } = new AddMessagesToInboxSettings();
11 |
12 | public CleanUpInboxSettings CleanUpInboxSettings { get; } = new CleanUpInboxSettings();
13 |
14 | public CleanUpIdempotencyKeysSettings CleanUpIdempotencyKeysSettings { get; } = new CleanUpIdempotencyKeysSettings();
15 |
16 | public Action ConfigureDeserialization { get; set; } = x => x.UseSystemTextJson();
17 |
18 | internal InboxSettings() { }
19 |
20 | internal void ConfigureDelegates(IServiceCollection services)
21 | {
22 | ConfigureDeserialization(new InboxDeserializationConfigurator(services));
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/samples/TransactionalBox.Sample.WebApi/TransactionalBox.Sample.WebApi.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0
5 | enable
6 | enable
7 | true
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/documentation/src/css/custom.css:
--------------------------------------------------------------------------------
1 | /**
2 | * Any CSS included here will be global. The classic template
3 | * bundles Infima by default. Infima is a CSS framework designed to
4 | * work well for content-centric websites.
5 | */
6 |
7 | /* You can override the default Infima variables here. */
8 | :root {
9 | --ifm-color-primary: #2e8555;
10 | --ifm-color-primary-dark: #29784c;
11 | --ifm-color-primary-darker: #277148;
12 | --ifm-color-primary-darkest: #205d3b;
13 | --ifm-color-primary-light: #33925d;
14 | --ifm-color-primary-lighter: #359962;
15 | --ifm-color-primary-lightest: #3cad6e;
16 | --ifm-code-font-size: 95%;
17 | --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1);
18 | }
19 |
20 | /* For readability concerns, you should choose a lighter palette in dark mode. */
21 | [data-theme='dark'] {
22 | --ifm-color-primary: #5575be;
23 | --ifm-color-primary-dark: #4466b3;
24 | --ifm-color-primary-darker: #4160a9;
25 | --ifm-color-primary-darkest: #354f8b;
26 | --ifm-color-primary-light: #6985c6;
27 | --ifm-color-primary-lighter: #738dc9;
28 | --ifm-color-primary-lightest: #91a5d5;
29 | }
30 |
--------------------------------------------------------------------------------
/source/TransactionalBox/Internals/InternalPackages/EventHooks/ExtensionAddEventHookHandler.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.DependencyInjection;
2 | using TransactionalBox.Internals.EventHooks.Internals.Loggers;
3 |
4 | namespace TransactionalBox.Internals.InternalPackages.EventHooks
5 | {
6 | internal static class ExtensionAddEventHookHandler
7 | {
8 | internal static void AddEventHookHandler(this IServiceCollection services)
9 | where THookListener : class, IEventHookHandler
10 | where THook : EventHook, new()
11 | {
12 | services.AddSingleton>();
13 |
14 | services.AddSingleton>();
15 |
16 | services.AddSingleton();
17 |
18 | services.AddScoped, THookListener>();
19 |
20 | services.AddSingleton(typeof(IHookListnerLogger), typeof(HookListnerLogger));
21 |
22 | services.AddHostedService();
23 | }
24 |
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Adrian Mikołajczyk
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 |
--------------------------------------------------------------------------------
/samples/Bank/TransactionalBox.BankAccounts/Messages/CreatedCustomerEventMessageHandler.cs:
--------------------------------------------------------------------------------
1 | using TransactionalBox.BankAccounts.Database;
2 | using TransactionalBox.BankAccounts.Models;
3 |
4 | namespace TransactionalBox.BankAccounts.Messages
5 | {
6 | public sealed class CreatedCustomerEventMessageHandler : IInboxHandler
7 | {
8 | private readonly BankAccountsDbContext _bankAccountsDbContext;
9 |
10 | public CreatedCustomerEventMessageHandler(BankAccountsDbContext bankAccountsDbContext)
11 | {
12 | _bankAccountsDbContext = bankAccountsDbContext;
13 | }
14 |
15 | public async Task Handle(CreatedCustomerEventMessage message, IExecutionContext executionContext)
16 | {
17 | var bankAccount = new BankAccount()
18 | {
19 | Id = Guid.NewGuid(),
20 | CustomerId = message.Id,
21 | Balance = 100,
22 | };
23 |
24 | await _bankAccountsDbContext.BankAccounts.AddAsync(bankAccount);
25 | await _bankAccountsDbContext.SaveChangesAsync();
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/source/TransactionalBox/Settings/Outbox/OutboxSettings.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.DependencyInjection;
2 | using TransactionalBox.Configurators.Outbox;
3 | using TransactionalBox.Internals.Outbox.Configurators;
4 |
5 | namespace TransactionalBox.Settings.Outbox
6 | {
7 | public sealed class OutboxSettings
8 | {
9 | public AddMessagesToTransportSettings AddMessagesToTransportSettings { get; } = new AddMessagesToTransportSettings();
10 |
11 | public CleanUpOutboxSettings CleanUpOutboxSettings { get; } = new CleanUpOutboxSettings();
12 |
13 | public Action ConfigureSerialization { get; set; } = x => x.UseSystemTextJson();
14 |
15 | public Action ConfigureCompression { get; set; } = x => x.UseNoCompression();
16 |
17 | internal OutboxSettings() { }
18 |
19 | internal void ConfigureDelegates(IServiceCollection services)
20 | {
21 | ConfigureSerialization(new OutboxSerializationConfigurator(services));
22 | ConfigureCompression(new OutboxCompressionConfigurator(services));
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/source/TransactionalBox/Internals/Outbox/Hooks/Handlers/AddMessagesToTransport/TransportMessageFactories/Policies/PayloadHasOptimalSizePolicy.cs:
--------------------------------------------------------------------------------
1 | using TransactionalBox.Internals.Outbox.Storage;
2 | using TransactionalBox.Internals.Outbox.Transport.ContractsToImplement;
3 |
4 | namespace TransactionalBox.Internals.Outbox.Hooks.Handlers.AddMessagesToTransport.TransportMessageFactories.Policies
5 | {
6 | internal sealed class PayloadHasOptimalSizePolicy : IPayloadCreationPolicy
7 | {
8 | private readonly ITransportMessageSizeSettings _settings;
9 |
10 | public PayloadHasOptimalSizePolicy(ITransportMessageSizeSettings settings)
11 | {
12 | _settings = settings;
13 | }
14 |
15 | public Task> Execute(byte[] compressedPayload, IEnumerable outboxMessages)
16 | {
17 | return Task.FromResult>(new List { compressedPayload });
18 | }
19 |
20 | public bool IsApplicable(int compressedPayloadSize)
21 | {
22 | return compressedPayloadSize <= _settings.OptimalTransportMessageSize;
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/tests/TransactionalBox.End2EndTests/TestCases/End2EndTestCase.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 | using Xunit.Abstractions;
7 |
8 | namespace TransactionalBox.End2EndTests.TestCases
9 | {
10 | public class End2EndTestCase
11 | {
12 | private readonly Func> _init;
13 |
14 | private readonly Func _cleanUp;
15 |
16 | private readonly string _testName;
17 |
18 | public End2EndTestCase(
19 | Func> init,
20 | Func cleanUp,
21 | string testName)
22 | {
23 | _init = init;
24 | _cleanUp = cleanUp;
25 | _testName = testName;
26 | }
27 |
28 | public Task Init(ITestOutputHelper testOutputHelper)
29 | {
30 | return _init(testOutputHelper);
31 | }
32 |
33 | public Task CleanUp()
34 | {
35 | return _cleanUp();
36 | }
37 |
38 | public override string ToString() => _testName;
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/source/TransactionalBox/Internals/Inbox/BackgroundProcesses/AddMessagesToInbox/Logger/AddMessagesToInboxLogger.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.Logging;
2 |
3 | namespace TransactionalBox.Internals.Inbox.BackgroundProcesses.AddMessagesToInbox.Logger
4 | {
5 | internal sealed partial class AddMessagesToInboxLogger : IAddMessagesToInboxLogger
6 | {
7 | private readonly ILogger _logger;
8 |
9 | public AddMessagesToInboxLogger(ILogger logger) => _logger = logger;
10 |
11 | [LoggerMessage(0, LogLevel.Error, "{name} (Attempt: {attempt} Delay: {msDelay}ms) unexpected exception", SkipEnabledCheck = true)]
12 | public partial void UnexpectedException(string name, long attempt, long msDelay, Exception exception);
13 |
14 | [LoggerMessage(0, LogLevel.Warning, "Detected duplicated messages with ids '{ids}'", SkipEnabledCheck = true)]
15 | public partial void DetectedDuplicatedMessages(string ids);
16 |
17 | [LoggerMessage(0, LogLevel.Information, "AddMessagesToInbox (NumberOfMessagesAdded: {numberOfMessages})", SkipEnabledCheck = true)]
18 | public partial void AddedMessagesToInbox(int numberOfMessages);
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/source/TransactionalBox/Internals/Outbox/Transport/InMemory/InMemoryOutboxTransport.cs:
--------------------------------------------------------------------------------
1 | using TransactionalBox.Internals.InternalPackages.Transport;
2 | using TransactionalBox.Internals.Outbox.Hooks.Handlers.AddMessagesToTransport.TransportMessageFactories;
3 | using TransactionalBox.Internals.Outbox.Transport.ContractsToImplement;
4 |
5 | namespace TransactionalBox.Internals.Outbox.Transport.InMemory
6 | {
7 | internal sealed class InMemoryOutboxTransport : IOutboxTransport
8 | {
9 | private readonly IInMemoryTransport _inMemoryTransport;
10 |
11 | public InMemoryOutboxTransport(IInMemoryTransport inMemoryTransport)
12 | {
13 | _inMemoryTransport = inMemoryTransport;
14 | }
15 |
16 | public async Task Add(TransportEnvelope transportEnvelope)
17 | {
18 | var transportObject = new TransportObject()
19 | {
20 | Topic = transportEnvelope.Topic,
21 | Payload = transportEnvelope.Payload,
22 | Compression = transportEnvelope.Compression,
23 | };
24 |
25 | await _inMemoryTransport.Writer.WriteAsync(transportObject);
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/source/TransactionalBox/Extensions/Outbox/OutboxExtensionUseGZip.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.DependencyInjection;
2 | using Microsoft.IO;
3 | using TransactionalBox.Settings.Outbox.Compression;
4 | using TransactionalBox.Internals.Outbox.Compression;
5 | using TransactionalBox.Internals.Outbox.Compression.GZip;
6 | using TransactionalBox.Configurators.Outbox;
7 |
8 | namespace TransactionalBox
9 | {
10 | public static class OutboxExtensionUseGZip
11 | {
12 | public static void UseGZip(
13 | this IOutboxCompressionConfigurator configurator,
14 | Action? settingsConfiguration = null)
15 | {
16 | var services = configurator.Services;
17 |
18 | var settings = new GZipCompressionSettings();
19 |
20 | if (settingsConfiguration is not null)
21 | {
22 | settingsConfiguration(settings);
23 | }
24 |
25 | services.AddSingleton(new RecyclableMemoryStreamManager());
26 |
27 | services.AddSingleton(settings);
28 |
29 | services.AddSingleton();
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/source/TransactionalBox/Internals/Inbox/Decompression/GZipDecompression.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.IO;
2 | using System.IO.Compression;
3 |
4 | namespace TransactionalBox.Internals.Inbox.Decompression
5 | {
6 | internal sealed class GZipDecompression : IDecompression
7 | {
8 | public string Name { get; } = "gzip";
9 |
10 | private readonly RecyclableMemoryStreamManager _streamManager;
11 |
12 | public GZipDecompression(RecyclableMemoryStreamManager streamManager)
13 | {
14 | _streamManager = streamManager;
15 | }
16 |
17 | public async Task Decompress(byte[] data)
18 | {
19 | using (var memoryStreamInput = _streamManager.GetStream(data))
20 | using (var memoryStreamOutput = _streamManager.GetStream())
21 | using (var gZipStream = new GZipStream(memoryStreamInput, CompressionMode.Decompress))
22 | {
23 | await gZipStream.CopyToAsync(memoryStreamOutput).ConfigureAwait(false);
24 |
25 | await gZipStream.FlushAsync().ConfigureAwait(false);
26 |
27 | return memoryStreamOutput.ToArray();
28 | }
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/source/TransactionalBox/Internals/InternalPackages/EventHooks/EventHookHub.cs:
--------------------------------------------------------------------------------
1 | using System.Threading.Channels;
2 |
3 | namespace TransactionalBox.Internals.InternalPackages.EventHooks
4 | {
5 | internal sealed class EventHookHub where TEventHook : EventHook, new()
6 | {
7 | private static Channel _channel = Channel.CreateBounded(new BoundedChannelOptions(1)
8 | {
9 | FullMode = BoundedChannelFullMode.DropOldest,
10 | SingleReader = true,
11 | SingleWriter = false,
12 | AllowSynchronousContinuations = false,
13 | });
14 |
15 | private ChannelWriter _writer => _channel.Writer;
16 |
17 | private ChannelReader _reader => _channel.Reader;
18 |
19 | private readonly TimeProvider _timeProvider;
20 |
21 | public EventHookHub(TimeProvider timeProvider) => _timeProvider = timeProvider;
22 |
23 | public ValueTask PublishAsync() => _writer.WriteAsync(_timeProvider.GetUtcNow().UtcDateTime);
24 |
25 | public IAsyncEnumerable ListenAsync(CancellationToken cancellationToken) => _reader.ReadAllAsync(cancellationToken);
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/source/TransactionalBox/Extensions/Outbox/OutboxExtensionUseBrotli.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.IO;
2 | using Microsoft.Extensions.DependencyInjection;
3 | using TransactionalBox.Settings.Outbox.Compression;
4 | using TransactionalBox.Internals.Outbox.Compression;
5 | using TransactionalBox.Internals.Outbox.Compression.Brotli;
6 | using TransactionalBox.Configurators.Outbox;
7 |
8 | namespace TransactionalBox
9 | {
10 | public static class OutboxExtensionUseBrotli
11 | {
12 | public static void UseBrotli(
13 | this IOutboxCompressionConfigurator configurator,
14 | Action? settingsConfiguration = null)
15 | {
16 | var services = configurator.Services;
17 |
18 | var settings = new BrotliCompressionSettings();
19 |
20 | if (settingsConfiguration is not null)
21 | {
22 | settingsConfiguration(settings);
23 | }
24 |
25 | services.AddSingleton(new RecyclableMemoryStreamManager());
26 |
27 | services.AddSingleton(settings);
28 |
29 | services.AddSingleton();
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/source/TransactionalBox/Internals/Outbox/Extensions/OutboxExtensionUseInMemoryStorage.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.DependencyInjection;
2 | using TransactionalBox.Internals.Outbox.Storage.InMemory;
3 | using TransactionalBox.Internals.Outbox.Storage.ContractsToImplement;
4 | using TransactionalBox.Configurators.Outbox;
5 | using TransactionalBox.Internals.InternalPackages.KeyedInMemoryLock;
6 |
7 | namespace TransactionalBox.Internals.Outbox.Extensions
8 | {
9 | internal static class OutboxExtensionUseInMemoryStorage
10 | {
11 | internal static void UseInMemoryStorage(this IOutboxStorageConfigurator configurator)
12 | {
13 | var services = configurator.Services;
14 |
15 | services.AddKeyedInMemoryLock();
16 | services.AddSingleton();
17 | services.AddSingleton();
18 | services.AddSingleton();
19 | services.AddSingleton();
20 | services.AddSingleton();
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/source/TransactionalBox/Internals/Inbox/Extensions/InboxExtensionUseInMemoryStorage.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.DependencyInjection;
2 | using TransactionalBox.Internals.Inbox.Storage.ContractsToImplement;
3 | using TransactionalBox.Internals.Inbox.Storage.InMemory;
4 | using TransactionalBox.Configurators.Inbox;
5 | using TransactionalBox.Internals.InternalPackages.KeyedInMemoryLock;
6 |
7 | namespace TransactionalBox.Internals.Inbox.Extensions
8 | {
9 | internal static class InboxExtensionUseInMemoryStorage
10 | {
11 | internal static void UseInMemoryStorage(this IInboxStorageConfigurator configurator)
12 | {
13 | var services = configurator.Services;
14 |
15 | services.AddKeyedInMemoryLock();
16 | services.AddSingleton();
17 | services.AddSingleton();
18 | services.AddSingleton();
19 | services.AddSingleton();
20 | services.AddSingleton();
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/tests/TransactionalBox.UnitTests/TransactionalBox.UnitTests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0
5 | enable
6 | enable
7 |
8 | false
9 | true
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | runtime; build; native; contentfiles; analyzers; buildtransitive
18 | all
19 |
20 |
21 | runtime; build; native; contentfiles; analyzers; buildtransitive
22 | all
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/.github/workflows/documentation-deploy.yaml:
--------------------------------------------------------------------------------
1 | name: Deploy documentation
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | paths:
8 | - '.github/workflows/documentation*.yaml'
9 | - 'documentation/**'
10 |
11 | jobs:
12 | deploy:
13 |
14 | name: Deploy documentation
15 | runs-on: ubuntu-latest
16 | permissions:
17 | contents: write
18 | concurrency:
19 | group: ${{ github.workflow }}-${{ github.ref }}
20 | defaults:
21 | run:
22 | working-directory: ./documentation
23 |
24 | steps:
25 | - uses: actions/checkout@v4
26 | with:
27 | fetch-depth: 0
28 |
29 | - uses: actions/setup-node@v4
30 | with:
31 | node-version: 18
32 | cache: npm
33 | cache-dependency-path: ./documentation/package-lock.json
34 |
35 | - name: Install dependencies
36 | run: npm ci
37 |
38 | - name: Build website
39 | run: npm run build
40 |
41 | - name: Deploy to GitHub Pages
42 | uses: peaceiris/actions-gh-pages@v3
43 | with:
44 | github_token: ${{ secrets.GITHUB_TOKEN }}
45 | publish_dir: ./documentation/build
46 | cname: transactionalbox.com
--------------------------------------------------------------------------------
/samples/Bank/TransactionalBox.Customers/Dockerfile:
--------------------------------------------------------------------------------
1 | #See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging.
2 |
3 | FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
4 | USER app
5 | WORKDIR /app
6 | EXPOSE 8080
7 | EXPOSE 8081
8 |
9 | FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
10 | ARG BUILD_CONFIGURATION=Release
11 | WORKDIR /src
12 | COPY ["samples/Directory.Packages.props", "samples/"]
13 | COPY ["samples/Bank/TransactionalBox.Customers/TransactionalBox.Customers.csproj", "samples/Bank/TransactionalBox.Customers/"]
14 | RUN dotnet restore "./samples/Bank/TransactionalBox.Customers/./TransactionalBox.Customers.csproj"
15 | COPY . .
16 | WORKDIR "/src/samples/Bank/TransactionalBox.Customers"
17 | RUN dotnet build "./TransactionalBox.Customers.csproj" -c $BUILD_CONFIGURATION -o /app/build
18 |
19 | FROM build AS publish
20 | ARG BUILD_CONFIGURATION=Release
21 | RUN dotnet publish "./TransactionalBox.Customers.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
22 |
23 | FROM base AS final
24 | WORKDIR /app
25 | COPY --from=publish /app/publish .
26 | ENTRYPOINT ["dotnet", "TransactionalBox.Customers.dll"]
--------------------------------------------------------------------------------
/source/TransactionalBox/Internals/Inbox/Decompression/BrotliDecompression.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.IO;
2 | using System.IO.Compression;
3 |
4 | namespace TransactionalBox.Internals.Inbox.Decompression
5 | {
6 | internal sealed class BrotliDecompression : IDecompression
7 | {
8 | public string Name { get; } = "brotli";
9 |
10 | private readonly RecyclableMemoryStreamManager _streamManager;
11 |
12 | public BrotliDecompression(
13 | RecyclableMemoryStreamManager streamManager)
14 | {
15 | _streamManager = streamManager;
16 | }
17 |
18 | public async Task Decompress(byte[] data)
19 | {
20 | using (var memoryStreamInput = _streamManager.GetStream(data))
21 | using (var memoryStreamOutput = _streamManager.GetStream())
22 | using (var brotliStream = new BrotliStream(memoryStreamInput, CompressionMode.Decompress))
23 | {
24 | await brotliStream.CopyToAsync(memoryStreamOutput).ConfigureAwait(false);
25 |
26 | await brotliStream.FlushAsync().ConfigureAwait(false);
27 |
28 | return memoryStreamOutput.ToArray();
29 | }
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/source/TransactionalBox/Internals/Outbox/Compression/GZip/GZipCompression.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.IO;
2 | using System.IO.Compression;
3 |
4 | namespace TransactionalBox.Internals.Outbox.Compression.GZip
5 | {
6 | internal sealed class GZipCompression : ICompression
7 | {
8 | public string Name { get; } = "gzip";
9 |
10 | private readonly IGZipCompressionSettings _settings;
11 |
12 | private readonly RecyclableMemoryStreamManager _streamManager;
13 |
14 | public GZipCompression(
15 | IGZipCompressionSettings settings,
16 | RecyclableMemoryStreamManager streamManager)
17 | {
18 | _settings = settings;
19 | _streamManager = streamManager;
20 | }
21 |
22 | public async Task Compress(byte[] data)
23 | {
24 | using (var memoryStreamOutput = _streamManager.GetStream())
25 | using (var gZipStream = new GZipStream(memoryStreamOutput, _settings.CompressionLevel))
26 | {
27 | await gZipStream.WriteAsync(data, 0, data.Length);
28 | await gZipStream.FlushAsync();
29 |
30 | return memoryStreamOutput.ToArray();
31 | }
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/documentation/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "documentation",
3 | "version": "0.0.0",
4 | "private": true,
5 | "scripts": {
6 | "docusaurus": "docusaurus",
7 | "start": "docusaurus start",
8 | "build": "docusaurus build",
9 | "swizzle": "docusaurus swizzle",
10 | "deploy": "docusaurus deploy",
11 | "clear": "docusaurus clear",
12 | "serve": "docusaurus serve",
13 | "write-translations": "docusaurus write-translations",
14 | "write-heading-ids": "docusaurus write-heading-ids"
15 | },
16 | "dependencies": {
17 | "@docusaurus/core": "3.1.1",
18 | "@docusaurus/preset-classic": "3.1.1",
19 | "@mdx-js/react": "^3.0.0",
20 | "clsx": "^2.0.0",
21 | "prism-react-renderer": "^2.3.0",
22 | "react": "^18.0.0",
23 | "react-dom": "^18.0.0"
24 | },
25 | "devDependencies": {
26 | "@docusaurus/module-type-aliases": "3.1.1",
27 | "@docusaurus/types": "3.1.1"
28 | },
29 | "browserslist": {
30 | "production": [
31 | ">0.5%",
32 | "not dead",
33 | "not op_mini all"
34 | ],
35 | "development": [
36 | "last 3 chrome version",
37 | "last 3 firefox version",
38 | "last 5 safari version"
39 | ]
40 | },
41 | "engines": {
42 | "node": ">=18.0"
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/source/TransactionalBox.Kafka/Exntesions/Outbox/OutboxExtensionUseKafka.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.DependencyInjection;
2 | using TransactionalBox.Configurators.Outbox;
3 | using TransactionalBox.Internals.Outbox.Transport.ContractsToImplement;
4 | using TransactionalBox.Kafka.Internals.Outbox;
5 | using TransactionalBox.Kafka.Internals.Outbox.ImplementedContracts;
6 | using TransactionalBox.Kafka.Settings.Outbox;
7 |
8 | namespace TransactionalBox
9 | {
10 | public static class OutboxExtensionUseKafka
11 | {
12 | public static void UseKafka(
13 | this IOutboxTransportConfigurator outboxWorkerTransportConfigurator,
14 | Action settingsConfiguration)
15 | {
16 | var services = outboxWorkerTransportConfigurator.Services;
17 | var settings = new OutboxKafkaSettings();
18 |
19 | settingsConfiguration(settings);
20 |
21 | services.AddSingleton(settings);
22 | services.AddSingleton(settings.TransportMessageSizeSettings);
23 |
24 | services.AddSingleton();
25 | services.AddScoped();
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/source/Directory.Build.props:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | true
5 |
6 |
7 |
8 | net8.0
9 | enable
10 | enable
11 | https://transactionalbox.com
12 | https://github.com/adimiko/TransactionalBox
13 | git
14 | $(AssemblyName)
15 | $(AssemblyName)
16 | small-logo.png
17 | Outbox and Inbox Pattern in .NET
18 | True
19 | MIT
20 |
21 |
22 |
23 |
24 | True
25 | \
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/source/TransactionalBox/Internals/Outbox/Compression/Brotli/BrotliCompression.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.IO;
2 | using System.IO.Compression;
3 |
4 | namespace TransactionalBox.Internals.Outbox.Compression.Brotli
5 | {
6 | internal sealed class BrotliCompression : ICompression
7 | {
8 | public string Name { get; } = "brotli";
9 |
10 | private readonly IBrotliCompressionSettings _settings;
11 |
12 | private readonly RecyclableMemoryStreamManager _streamManager;
13 |
14 | public BrotliCompression(
15 | IBrotliCompressionSettings settings,
16 | RecyclableMemoryStreamManager streamManager)
17 | {
18 | _settings = settings;
19 | _streamManager = streamManager;
20 | }
21 |
22 | public async Task Compress(byte[] data)
23 | {
24 | using (var memoryStreamOutput = _streamManager.GetStream())
25 | using (var brotliStream = new BrotliStream(memoryStreamOutput, _settings.CompressionLevel))
26 | {
27 | await brotliStream.WriteAsync(data, 0, data.Length);
28 | await brotliStream.FlushAsync();
29 |
30 | return memoryStreamOutput.ToArray();
31 | }
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/samples/Bank/TransactionalBox.CustomerRegistrations/TransactionalBox.CustomerRegistrations.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0
5 | enable
6 | enable
7 | true
8 | de7ed6e4-a1eb-483b-9909-be15dca480ca
9 | Linux
10 | ..\..\..
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/samples/TransactionalBox.Sample.WebApi/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "http://json.schemastore.org/launchsettings.json",
3 | "iisSettings": {
4 | "windowsAuthentication": false,
5 | "anonymousAuthentication": true,
6 | "iisExpress": {
7 | "applicationUrl": "http://localhost:63254",
8 | "sslPort": 44313
9 | }
10 | },
11 | "profiles": {
12 | "http": {
13 | "commandName": "Project",
14 | "dotnetRunMessages": true,
15 | "launchBrowser": true,
16 | "launchUrl": "swagger",
17 | "applicationUrl": "http://localhost:5036",
18 | "environmentVariables": {
19 | "ASPNETCORE_ENVIRONMENT": "Development"
20 | }
21 | },
22 | "https": {
23 | "commandName": "Project",
24 | "dotnetRunMessages": true,
25 | "launchBrowser": true,
26 | "launchUrl": "swagger",
27 | "applicationUrl": "https://localhost:7108;http://localhost:5036",
28 | "environmentVariables": {
29 | "ASPNETCORE_ENVIRONMENT": "Development"
30 | }
31 | },
32 | "IIS Express": {
33 | "commandName": "IISExpress",
34 | "launchBrowser": true,
35 | "launchUrl": "swagger",
36 | "environmentVariables": {
37 | "ASPNETCORE_ENVIRONMENT": "Development"
38 | }
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/samples/TransactionalBox.Sample.WebApi.InMemory/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "http://json.schemastore.org/launchsettings.json",
3 | "iisSettings": {
4 | "windowsAuthentication": false,
5 | "anonymousAuthentication": true,
6 | "iisExpress": {
7 | "applicationUrl": "http://localhost:1454",
8 | "sslPort": 44334
9 | }
10 | },
11 | "profiles": {
12 | "http": {
13 | "commandName": "Project",
14 | "dotnetRunMessages": true,
15 | "launchBrowser": true,
16 | "launchUrl": "swagger",
17 | "applicationUrl": "http://localhost:5152",
18 | "environmentVariables": {
19 | "ASPNETCORE_ENVIRONMENT": "Development"
20 | }
21 | },
22 | "https": {
23 | "commandName": "Project",
24 | "dotnetRunMessages": true,
25 | "launchBrowser": true,
26 | "launchUrl": "swagger",
27 | "applicationUrl": "https://localhost:7070;http://localhost:5152",
28 | "environmentVariables": {
29 | "ASPNETCORE_ENVIRONMENT": "Development"
30 | }
31 | },
32 | "IIS Express": {
33 | "commandName": "IISExpress",
34 | "launchBrowser": true,
35 | "launchUrl": "swagger",
36 | "environmentVariables": {
37 | "ASPNETCORE_ENVIRONMENT": "Development"
38 | }
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/samples/Bank/TransactionalBox.Loans/TransactionalBox.Loans.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0
5 | enable
6 | enable
7 | true
8 | fadd0aae-2531-4163-9e3b-e9c117fa776b
9 | Linux
10 | ..\..\..
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/.github/workflows/build-and-tests.yaml:
--------------------------------------------------------------------------------
1 | name: Build & Tests
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | paths:
8 | - '.github/workflows/build-and-tests.yaml'
9 | - 'samples/**'
10 | - 'source/**'
11 | - 'tests/**'
12 | pull_request:
13 | branches:
14 | - main
15 | paths:
16 | - '.github/workflows/build-and-tests.yaml'
17 | - 'samples/**'
18 | - 'source/**'
19 | - 'tests/**'
20 |
21 | jobs:
22 |
23 | build:
24 |
25 | name: Build & Tests
26 |
27 | runs-on: ubuntu-latest
28 |
29 | steps:
30 |
31 | - uses: actions/checkout@v4
32 |
33 | - name: Setup dotnet
34 | uses: actions/setup-dotnet@v4
35 | with:
36 | dotnet-version: '8.x.x'
37 |
38 | - name: Setup Docker
39 | uses: docker/setup-buildx-action@v3.0.0
40 |
41 | - name: Cache
42 | uses: actions/cache@v3
43 | with:
44 | path: ~/.nuget/packages
45 | key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj') }}
46 | restore-keys: |
47 | ${{ runner.os }}-nuget
48 |
49 | - name: Install dependencies
50 | run: dotnet restore
51 |
52 | - name: Build
53 | run: dotnet build --no-restore --configuration Release
54 |
55 | - name: Test
56 | run: dotnet test --no-restore --configuration Release
57 |
--------------------------------------------------------------------------------
/source/TransactionalBox.Kafka/Exntesions/Inbox/InboxExtensionUseKafka.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.DependencyInjection;
2 | using TransactionalBox.Kafka.Settings.Inbox;
3 | using TransactionalBox.Kafka.Internals.Inbox;
4 | using TransactionalBox.Kafka.Internals.Inbox.ImplementedContracts;
5 | using TransactionalBox.Configurators.Inbox;
6 | using TransactionalBox.Internals.Inbox.Transport.ContractsToImplement;
7 |
8 | namespace TransactionalBox
9 | {
10 | public static class InboxExtensionUseKafka
11 | {
12 | public static void UseKafka(
13 | this IInboxTransportConfigurator inboxWorkerTransportConfigurator,
14 | Action settingsConfiguration = null)
15 | {
16 | var services = inboxWorkerTransportConfigurator.Services;
17 | var settings = new InboxKafkaSettings();
18 |
19 | if (settingsConfiguration is not null)
20 | {
21 | settingsConfiguration(settings);
22 | }
23 |
24 | services.AddSingleton(settings);
25 | services.AddSingleton();
26 | services.AddSingleton();
27 | services.AddSingleton();
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/samples/Bank/TransactionalBox.Customers/TransactionalBox.Customers.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0
5 | enable
6 | enable
7 | false
8 | 6b456489-9b3e-4df4-9cd4-d04b03067250
9 | Linux
10 | ..\..\..
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/samples/Bank/Internals/BankLogger/LoggerExtension.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using Serilog.Events;
3 | using Serilog;
4 | using Serilog.Sinks.Elasticsearch;
5 | using Microsoft.AspNetCore.Builder;
6 | using Microsoft.Extensions.Configuration;
7 |
8 | namespace BankLogger
9 | {
10 | public static class LoggerExtension
11 | {
12 | public static void AddBankLogger(this WebApplicationBuilder builder, IConfiguration configuration, Assembly assembly)
13 | {
14 | Log.Logger = new LoggerConfiguration()
15 | .MinimumLevel.Override("Microsoft", LogEventLevel.Information)
16 | .Enrich.FromLogContext()
17 | .WriteTo.Console()
18 | .WriteTo.Debug()
19 | .WriteTo.Elasticsearch(new ElasticsearchSinkOptions(new Uri("http://elasticsearch:9200"))
20 | {
21 | AutoRegisterTemplate = true,
22 | //AutoRegisterTemplateVersion = AutoRegisterTemplateVersion.ESv6,
23 | IndexFormat = $"{assembly.GetName().Name!.ToLower().Replace(".", "-")}-{DateTime.UtcNow:yyyy-MM}",
24 | NumberOfReplicas = 1,
25 | NumberOfShards = 2
26 | })
27 | .ReadFrom.Configuration(configuration)
28 | .CreateLogger();
29 |
30 | builder.Host.UseSerilog();
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/samples/Bank/TransactionalBox.BankAccounts/TransactionalBox.BankAccounts.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0
5 | enable
6 | enable
7 | true
8 | 061812cd-fafc-4ebb-9ae9-3339594f2d33
9 | Linux
10 | ..\..\..
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/source/TransactionalBox/TransactionalBox.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/tests/Directory.Packages.props:
--------------------------------------------------------------------------------
1 |
2 |
3 | true
4 | false
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/source/TransactionalBox.EntityFrameworkCore/Internals/InternalPackages/DistributedLock/ExtensionUseEntityFrameworkCore.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore;
2 | using Microsoft.Extensions.DependencyInjection;
3 | using TransactionalBox.Internals.InternalPackages.DistributedLock;
4 |
5 | namespace TransactionalBox.EntityFrameworkCore.Internals.InternalPackages.DistributedLock
6 | {
7 | internal static class ExtensionUseEntityFrameworkCore
8 | {
9 | internal static IServiceCollection UseEntityFrameworkCore(
10 | this IDistributedLockStorageConfigurator storageConfigurator)
11 | where TDbContext : DbContext
12 | {
13 | var services = storageConfigurator.Services;
14 |
15 | services.AddScoped(sp => sp.GetRequiredService());
16 | services.AddScoped();
17 |
18 | return services;
19 | }
20 |
21 | internal static IServiceCollection UseEntityFrameworkCore(
22 | this IDistributedLockStorageConfigurator storageConfigurator)
23 | {
24 | var services = storageConfigurator.Services;
25 |
26 | services.AddScoped();
27 |
28 | return services;
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/source/TransactionalBox/Internals/Inbox/Assemblies/MessageTypes/InboxMessageTypes.cs:
--------------------------------------------------------------------------------
1 | namespace TransactionalBox.Internals.Inbox.Assemblies.MessageTypes
2 | {
3 | internal sealed class InboxMessageTypes : IInboxMessageTypes
4 | {
5 | private readonly Dictionary _dictionaryMessageTypes = new Dictionary();
6 |
7 | private readonly HashSet _messageTypes = new HashSet();
8 |
9 | public IReadOnlyDictionary DictionaryMessageTypes => _dictionaryMessageTypes;
10 |
11 | public IEnumerable MessageTypes => _messageTypes;
12 |
13 | internal InboxMessageTypes(IEnumerable inboxMessageHandlerTypes, Type handlerGenericType)
14 | {
15 | foreach (Type type in inboxMessageHandlerTypes)
16 | {
17 | var handlerTypes = type
18 | .GetInterfaces()
19 | .Where(t => t.IsGenericType && t.GetGenericTypeDefinition() == handlerGenericType);
20 |
21 | foreach (Type handlerType in handlerTypes)
22 | {
23 | var messageType = handlerType.GetGenericArguments()[0];
24 |
25 | _dictionaryMessageTypes.Add(messageType.Name, messageType);
26 | _messageTypes.Add(messageType);
27 | }
28 | }
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/source/TransactionalBox/Internals/InternalPackages/AssemblyConfigurator/AssemblyConfigurator.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using TransactionalBox.Configurators;
3 |
4 | namespace TransactionalBox.Internals.InternalPackages.AssemblyConfigurator
5 | {
6 | internal sealed class AssemblyConfigurator : IAssemblyConfigurator
7 | {
8 | private readonly ISet _assemblies = new HashSet();
9 |
10 | internal IEnumerable Assemblies
11 | {
12 | get
13 | {
14 | if (_assemblies.Any())
15 | {
16 | return _assemblies;
17 | }
18 |
19 | var assemblies = AppDomain.CurrentDomain.GetAssemblies();
20 |
21 | foreach (var assembly in assemblies)
22 | {
23 | _assemblies.Add(assembly);
24 | }
25 |
26 | return _assemblies;
27 | }
28 | }
29 |
30 | public void RegisterFromAssemblies(Assembly assembly)
31 | {
32 | _assemblies.Add(assembly);
33 | }
34 |
35 | public void RegisterFromAssemblies(params Assembly[] assemblies)
36 | {
37 | foreach (Assembly assembly in assemblies)
38 | {
39 | _assemblies.Add(assembly);
40 | }
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/samples/TransactionalBox.Sample.WebApi.InMemory/Program.cs:
--------------------------------------------------------------------------------
1 | using TransactionalBox;
2 | using TransactionalBox.Sample.WebApi.InMemory.ServiceWithOutbox;
3 | using TransactionalBox.Internals.Outbox.Storage.InMemory;
4 | using TransactionalBox.Internals.Inbox.Storage.InMemory;
5 |
6 | var builder = WebApplication.CreateBuilder(args);
7 |
8 | builder.Services.AddEndpointsApiExplorer();
9 | builder.Services.AddSwaggerGen();
10 | builder.Services.AddScoped();
11 |
12 | builder.Services.AddTransactionalBox(x =>
13 | {
14 | x.AddOutbox();
15 |
16 | x.AddInbox();
17 | },
18 | configuration: builder.Configuration);
19 |
20 | var app = builder.Build();
21 |
22 | // Configure the HTTP request pipeline.
23 | if (app.Environment.IsDevelopment())
24 | {
25 | app.UseSwagger();
26 | app.UseSwaggerUI();
27 | }
28 |
29 | app.UseHttpsRedirection();
30 |
31 | app.MapPost("/add-message-to-outbox", async (ExampleServiceWithOutbox exampleServiceWithOutbox) => await exampleServiceWithOutbox.Execute());
32 |
33 | app.MapGet("/get-messages-from-outbox", (IOutboxStorageReadOnly outboxStorageReadOnly) => outboxStorageReadOnly.OutboxMessages);
34 |
35 | app.MapGet("/get-messages-from-inbox", (IInboxStorageReadOnly inboxStorage) => inboxStorage.InboxMessages);
36 |
37 | app.MapGet("/get-idempotent-messages-from-inbox", (IInboxStorageReadOnly inboxStorage) => inboxStorage.IdempotentInboxKeys);
38 |
39 | app.Run();
40 |
--------------------------------------------------------------------------------
/source/TransactionalBox.Kafka/Internals/Inbox/KafkaConsumerConfigFactory.cs:
--------------------------------------------------------------------------------
1 | using Confluent.Kafka;
2 | using TransactionalBox.Internals;
3 |
4 | namespace TransactionalBox.Kafka.Internals.Inbox
5 | {
6 | internal sealed class KafkaConsumerConfigFactory
7 | {
8 | private readonly IServiceContext _serviceContext;
9 |
10 | private readonly IInboxKafkaSettings _inboxWorkerKafkaSettings;
11 |
12 | private ConsumerConfig? _config = null;
13 |
14 | public KafkaConsumerConfigFactory(
15 | IServiceContext serviceContext,
16 | IInboxKafkaSettings inboxWorkerKafkaSettings)
17 | {
18 | _serviceContext = serviceContext;
19 | _inboxWorkerKafkaSettings = inboxWorkerKafkaSettings;
20 | }
21 |
22 | internal ConsumerConfig Create()
23 | {
24 | if (_config is not null)
25 | {
26 | return _config;
27 | }
28 |
29 | _config = new ConsumerConfig()
30 | {
31 | ClientId = _serviceContext.Id + Guid.NewGuid().ToString(), //TODO
32 | GroupId = _serviceContext.Id,
33 | BootstrapServers = _inboxWorkerKafkaSettings.BootstrapServers,
34 | AutoOffsetReset = AutoOffsetReset.Earliest,
35 | EnableAutoCommit = false,
36 | };
37 |
38 | return _config;
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------