├── src ├── Events │ ├── Kafka │ │ ├── KafkaOrderEventConsumerSettings.cs │ │ ├── GuidSerializer.cs │ │ ├── JsonEventSerializer.cs │ │ └── KafkaEventConsumer.cs │ ├── EventBase.cs │ ├── IEventConsumer.cs │ ├── Events.csproj │ └── SampleEvents.cs ├── Consumer │ ├── appsettings.json │ ├── appsettings.Development.json │ ├── Properties │ │ └── launchSettings.json │ ├── .dockerignore │ ├── Program.cs │ ├── Dockerfile │ ├── Consumer.csproj │ └── Worker.cs └── Producer │ ├── appsettings.json │ ├── appsettings.Development.json │ ├── Properties │ └── launchSettings.json │ ├── .dockerignore │ ├── Data │ ├── SampleDbContext.cs │ ├── OutboxMessage.cs │ └── OutboxMessageConfiguration.cs │ ├── Dockerfile │ ├── Program.cs │ ├── Producer.csproj │ ├── debezium-outbox-config.json │ ├── DemoSetupHostedService.cs │ └── Worker.cs ├── README.md ├── LICENSE ├── docker-compose.yml ├── DebeziumOutboxSample.sln ├── .gitignore └── .editorconfig /src/Events/Kafka/KafkaOrderEventConsumerSettings.cs: -------------------------------------------------------------------------------- 1 | namespace Events.Kafka; 2 | 3 | public record KafkaOrderEventConsumerSettings(string ConsumerGroup); -------------------------------------------------------------------------------- /src/Events/EventBase.cs: -------------------------------------------------------------------------------- 1 | namespace Events; 2 | 3 | public abstract record EventBase(Guid Id, DateTime OccurredAt) 4 | { 5 | public abstract string Type { get; } 6 | } -------------------------------------------------------------------------------- /src/Events/IEventConsumer.cs: -------------------------------------------------------------------------------- 1 | namespace Events; 2 | 3 | public interface IEventConsumer 4 | { 5 | Task Subscribe(Action callback, CancellationToken ct); 6 | } -------------------------------------------------------------------------------- /src/Consumer/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.Hosting.Lifetime": "Information" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/Producer/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.Hosting.Lifetime": "Information" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/Consumer/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.Hosting.Lifetime": "Information" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/Producer/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.Hosting.Lifetime": "Information" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/Consumer/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "Consumer": { 4 | "commandName": "Project", 5 | "dotnetRunMessages": true, 6 | "environmentVariables": { 7 | "DOTNET_ENVIRONMENT": "Development" 8 | } 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/Producer/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "Producer": { 4 | "commandName": "Project", 5 | "dotnetRunMessages": true, 6 | "environmentVariables": { 7 | "DOTNET_ENVIRONMENT": "Development" 8 | } 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/Consumer/.dockerignore: -------------------------------------------------------------------------------- 1 | **/.dockerignore 2 | **/.env 3 | **/.git 4 | **/.gitignore 5 | **/.project 6 | **/.settings 7 | **/.toolstarget 8 | **/.vs 9 | **/.vscode 10 | **/.idea 11 | **/*.*proj.user 12 | **/*.dbmdl 13 | **/*.jfm 14 | **/azds.yaml 15 | **/bin 16 | **/charts 17 | **/docker-compose* 18 | **/Dockerfile* 19 | **/node_modules 20 | **/npm-debug.log 21 | **/obj 22 | **/secrets.dev.yaml 23 | **/values.dev.yaml 24 | LICENSE 25 | README.md -------------------------------------------------------------------------------- /src/Producer/.dockerignore: -------------------------------------------------------------------------------- 1 | **/.dockerignore 2 | **/.env 3 | **/.git 4 | **/.gitignore 5 | **/.project 6 | **/.settings 7 | **/.toolstarget 8 | **/.vs 9 | **/.vscode 10 | **/.idea 11 | **/*.*proj.user 12 | **/*.dbmdl 13 | **/*.jfm 14 | **/azds.yaml 15 | **/bin 16 | **/charts 17 | **/docker-compose* 18 | **/Dockerfile* 19 | **/node_modules 20 | **/npm-debug.log 21 | **/obj 22 | **/secrets.dev.yaml 23 | **/values.dev.yaml 24 | LICENSE 25 | README.md -------------------------------------------------------------------------------- /src/Events/Events.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/Events/Kafka/GuidSerializer.cs: -------------------------------------------------------------------------------- 1 | using Confluent.Kafka; 2 | 3 | namespace Events.Kafka; 4 | 5 | public class GuidSerializer: ISerializer, IDeserializer 6 | { 7 | private GuidSerializer() 8 | { 9 | } 10 | 11 | public static GuidSerializer Instance { get; } = new(); 12 | 13 | public byte[] Serialize(Guid data, SerializationContext context) => data.ToByteArray(); 14 | 15 | public Guid Deserialize(ReadOnlySpan data, bool isNull, SerializationContext context) => new(data); 16 | } -------------------------------------------------------------------------------- /src/Consumer/Program.cs: -------------------------------------------------------------------------------- 1 | using Consumer; 2 | using Events; 3 | using Events.Kafka; 4 | 5 | IHost host = Host.CreateDefaultBuilder(args) 6 | .ConfigureServices(services => 7 | { 8 | services.AddSingleton(new KafkaOrderEventConsumerSettings("consumer")); 9 | services.AddSingleton(); 10 | services.AddSingleton(typeof(JsonEventSerializer<>)); 11 | services.AddHostedService(); 12 | services.AddLogging(loggingBuilder => loggingBuilder.AddSeq(serverUrl: "http://seq:5341")); 13 | }) 14 | .Build(); 15 | 16 | await host.RunAsync(); -------------------------------------------------------------------------------- /src/Producer/Data/SampleDbContext.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | 3 | namespace Producer.Data; 4 | 5 | public class SampleDbContext : DbContext 6 | { 7 | #pragma warning disable CS8618 // initialized by EF 8 | public SampleDbContext(DbContextOptions options) : base(options) 9 | #pragma warning restore CS8618 10 | { 11 | } 12 | 13 | public DbSet OutboxMessages { get; set; } 14 | 15 | protected override void OnModelCreating(ModelBuilder modelBuilder) 16 | { 17 | modelBuilder.ApplyConfigurationsFromAssembly(GetType().Assembly); 18 | } 19 | } -------------------------------------------------------------------------------- /src/Consumer/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base 2 | WORKDIR /app 3 | EXPOSE 80 4 | EXPOSE 443 5 | 6 | FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build 7 | WORKDIR /src 8 | COPY ["src/Consumer/Consumer.csproj", "Consumer/"] 9 | COPY ["src/Events/Events.csproj", "Events/"] 10 | RUN dotnet restore "Consumer/Consumer.csproj" 11 | COPY ["src/Consumer/.", "Consumer/"] 12 | COPY ["src/Events/.", "Events/"] 13 | WORKDIR "/src/Consumer" 14 | 15 | FROM build AS publish 16 | RUN dotnet publish "Consumer.csproj" -c Release -o /app/publish 17 | 18 | FROM base AS final 19 | WORKDIR /app 20 | COPY --from=publish /app/publish . 21 | ENTRYPOINT ["dotnet", "Consumer.dll"] 22 | -------------------------------------------------------------------------------- /src/Producer/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base 2 | WORKDIR /app 3 | EXPOSE 80 4 | EXPOSE 443 5 | 6 | FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build 7 | WORKDIR /src 8 | COPY ["src/Producer/Producer.csproj", "Producer/"] 9 | COPY ["src/Events/Events.csproj", "Events/"] 10 | RUN dotnet restore "Producer/Producer.csproj" 11 | COPY ["src/Producer/.", "Producer/"] 12 | COPY ["src/Events/.", "Events/"] 13 | WORKDIR "/src/Producer" 14 | 15 | FROM build AS publish 16 | RUN dotnet publish "Producer.csproj" -c Release -o /app/publish 17 | 18 | FROM base AS final 19 | WORKDIR /app 20 | COPY --from=publish /app/publish . 21 | ENTRYPOINT ["dotnet", "Producer.dll"] 22 | -------------------------------------------------------------------------------- /src/Producer/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Producer; 3 | using Producer.Data; 4 | 5 | IHost host = Host.CreateDefaultBuilder(args) 6 | .ConfigureServices(services => 7 | { 8 | services.AddHostedService(); 9 | services.AddHostedService(); 10 | services.AddDbContext( 11 | options => options.UseNpgsql( 12 | "server=postgres;port=5432;user id=user;password=pass;database=DebeziumOutboxSample")); 13 | services.AddLogging(loggingBuilder => loggingBuilder.AddSeq(serverUrl: "http://seq:5341")); 14 | }) 15 | .Build(); 16 | 17 | await host.RunAsync(); -------------------------------------------------------------------------------- /src/Consumer/Consumer.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | enable 6 | enable 7 | dotnet-Consumer-E7B7B3F7-9447-41CE-B686-F2C28C843FCE 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/Consumer/Worker.cs: -------------------------------------------------------------------------------- 1 | using Events; 2 | 3 | namespace Consumer; 4 | 5 | public class Worker : BackgroundService 6 | { 7 | private readonly IEventConsumer _eventConsumer; 8 | private readonly ILogger _logger; 9 | 10 | public Worker(IEventConsumer eventConsumer, ILogger logger) 11 | { 12 | _eventConsumer = eventConsumer; 13 | _logger = logger; 14 | } 15 | 16 | protected override Task ExecuteAsync(CancellationToken stoppingToken) 17 | => _eventConsumer.Subscribe(ConsumeEvent, stoppingToken); 18 | 19 | private void ConsumeEvent(OrderEventBase @event) 20 | => _logger.LogInformation("Event of type {eventType} received by the consumer application", @event.Type); 21 | } -------------------------------------------------------------------------------- /src/Producer/Data/OutboxMessage.cs: -------------------------------------------------------------------------------- 1 | using Events; 2 | 3 | namespace Producer.Data; 4 | 5 | public class OutboxMessage 6 | { 7 | public OutboxMessage(EventBase payload, string aggregateId, string aggregateType) 8 | { 9 | Id = payload.Id; 10 | Payload = payload; 11 | AggregateId = aggregateId; 12 | AggregateType = aggregateType; 13 | Timestamp = DateTime.SpecifyKind(payload.OccurredAt, DateTimeKind.Unspecified); 14 | Type = payload.Type; 15 | } 16 | 17 | public Guid Id { get; } 18 | 19 | public EventBase Payload { get; } 20 | 21 | public string AggregateId { get; } 22 | 23 | public string AggregateType { get; } 24 | 25 | public string Type { get; } 26 | 27 | public DateTime Timestamp { get; } 28 | } -------------------------------------------------------------------------------- /src/Producer/Data/OutboxMessageConfiguration.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 3 | 4 | namespace Producer.Data; 5 | 6 | public class OutboxMessageConfiguration: IEntityTypeConfiguration 7 | { 8 | public void Configure(EntityTypeBuilder builder) 9 | { 10 | builder.HasKey(e => e.Id); 11 | 12 | builder 13 | .Property(e => e.Payload) 14 | .HasColumnType("jsonb"); 15 | 16 | // required for EF to map bind them in the ctor, even though the properties are readonly 17 | builder.Property(e => e.AggregateId); 18 | builder.Property(e => e.AggregateType); 19 | builder.Property(e => e.Type); 20 | builder.Property(e => e.Timestamp).HasColumnType("Timestamp"); 21 | } 22 | } -------------------------------------------------------------------------------- /src/Producer/Producer.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | enable 6 | enable 7 | Linux 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/Events/SampleEvents.cs: -------------------------------------------------------------------------------- 1 | namespace Events; 2 | 3 | public abstract record OrderEventBase(Guid Id, DateTime OccurredAt, Guid OrderId, Guid DishId, string CustomerNumber) 4 | : EventBase(Id, OccurredAt); 5 | 6 | public record OrderCreated(Guid Id, DateTime OccurredAt, Guid OrderId, Guid DishId, string CustomerNumber) 7 | : OrderEventBase(Id, OccurredAt, OrderId, DishId, CustomerNumber) 8 | { 9 | public override string Type => nameof(OrderCreated); 10 | } 11 | 12 | public record OrderDelivered(Guid Id, DateTime OccurredAt, Guid OrderId, Guid DishId, string CustomerNumber) 13 | : OrderEventBase(Id, OccurredAt, OrderId, DishId, CustomerNumber) 14 | { 15 | public override string Type => nameof(OrderDelivered); 16 | } 17 | 18 | public record OrderCancelled(Guid Id, DateTime OccurredAt, Guid OrderId, Guid DishId, string CustomerNumber, string? Reason) 19 | : OrderEventBase(Id, OccurredAt, OrderId, DishId, CustomerNumber) 20 | { 21 | public override string Type => nameof(OrderCancelled); 22 | } 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Debezium Outbox Sample 2 | 3 | Tiny sample application showing how we could use Debezium to publish our outbox messages 4 | 5 | **NOTE** It should go without saying that this **is not production ready**. 6 | 7 | ## About the solution 8 | 9 | The solution is comprised of 3 projects 10 | 11 | - Producer - Application that stores a collection of messages in the outbox table. Doesn't do anything else, as Debezium is in charge of doing the actual publishing. 12 | - Consumer - Subscribes to events, logging every time a new one arrives, just to show that the messages put in the outbox are being published to Kafka. 13 | - Events - Contains the events that are used in the solution. Also contains interfaces (and implementations) for publishing and subscribing to events. 14 | 15 | In the root of the solution, there's a Docker Compose file to spin up the necessary dependencies, which are PostgreSQL, Kafka and Seq. 16 | 17 | Using JSON serialization for the events, good enough for demo purposes. For production scenarios, something like ProtoBuf or Avro are probably better options. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 João Antunes 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 | -------------------------------------------------------------------------------- /src/Producer/debezium-outbox-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "connector.class": "io.debezium.connector.postgresql.PostgresConnector", 3 | "tasks.max": "1", 4 | "database.hostname": "postgres", 5 | "database.port": "5432", 6 | "database.user": "user", 7 | "database.password": "pass", 8 | "database.dbname": "DebeziumOutboxSample", 9 | "database.server.name": "postgres", 10 | "schema.include.list": "public", 11 | "table.include.list": "public.OutboxMessages", 12 | "tombstones.on.delete": "false", 13 | "transforms": "outbox", 14 | "transforms.outbox.type": "io.debezium.transforms.outbox.EventRouter", 15 | "transforms.outbox.table.field.event.id": "Id", 16 | "transforms.outbox.table.field.event.key": "AggregateId", 17 | "transforms.outbox.table.field.event.timestamp": "Timestamp", 18 | "transforms.outbox.table.field.event.payload": "Payload", 19 | "transforms.outbox.table.expand.json.payload": true, 20 | "transforms.outbox.route.by.field": "AggregateType", 21 | "transforms.outbox.route.topic.replacement": "${routedByValue}.events", 22 | "transforms.outbox.table.fields.additional.placement": "Type:header:eventType", 23 | "value.converter":"org.apache.kafka.connect.json.JsonConverter", 24 | "value.converter.schemas.enable": false, 25 | "plugin.name": "pgoutput", 26 | "topic.prefix": "PgDbSample" 27 | } -------------------------------------------------------------------------------- /src/Events/Kafka/JsonEventSerializer.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | using System.Text.Json; 3 | using Confluent.Kafka; 4 | 5 | namespace Events.Kafka; 6 | 7 | public class JsonEventSerializer : ISerializer, IDeserializer where T : class 8 | { 9 | private readonly Dictionary _eventTypeMap; 10 | 11 | public JsonEventSerializer() 12 | { 13 | _eventTypeMap = typeof(T) 14 | .Assembly 15 | .GetTypes() 16 | .Where(t => !t.IsAbstract && t.IsSubclassOf(typeof(T))) 17 | .ToDictionary(t => t.Name, t => t); 18 | } 19 | 20 | public byte[] Serialize(T data, SerializationContext context) 21 | => Encoding.UTF8.GetBytes(JsonSerializer.Serialize(data)); 22 | 23 | public T Deserialize(ReadOnlySpan data, bool isNull, SerializationContext context) 24 | { 25 | if (isNull) 26 | { 27 | // 🤷‍♂️library didn't really take nulls into account 28 | return null!; 29 | // in any case, shouldn't happen, we're not supposed to have empty events 30 | } 31 | 32 | var eventType = Encoding.UTF8.GetString(context.Headers.GetLastBytes("eventType")); 33 | var deserialized = (T) JsonSerializer.Deserialize(data, _eventTypeMap[eventType])!; 34 | return deserialized; 35 | } 36 | } -------------------------------------------------------------------------------- /src/Producer/DemoSetupHostedService.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Mime; 2 | using System.Text; 3 | using Polly; 4 | using Producer.Data; 5 | 6 | namespace Producer; 7 | 8 | public class DemoSetupHostedService : IHostedService 9 | { 10 | private readonly IServiceScopeFactory _scopeFactory; 11 | private readonly ILogger _logger; 12 | 13 | public DemoSetupHostedService(IServiceScopeFactory scopeFactory, ILogger logger) 14 | { 15 | _scopeFactory = scopeFactory; 16 | _logger = logger; 17 | } 18 | 19 | public async Task StartAsync(CancellationToken cancellationToken) 20 | { 21 | await InitializeDatabaseAsync(); 22 | 23 | await InitializeDebeziumAsync(); 24 | 25 | async Task InitializeDatabaseAsync() 26 | { 27 | using var scope = _scopeFactory.CreateScope(); 28 | var db = scope 29 | .ServiceProvider 30 | .GetRequiredService(); 31 | 32 | await db 33 | .Database 34 | .EnsureCreatedAsync(cancellationToken); 35 | } 36 | 37 | async Task InitializeDebeziumAsync() 38 | { 39 | var policy = Policy 40 | .Handle() 41 | .WaitAndRetryForeverAsync(retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt))); 42 | 43 | using var httpClient = new HttpClient(); 44 | 45 | await policy.ExecuteAsync( 46 | async () => 47 | { 48 | var response = await httpClient.PutAsync( 49 | "http://connect:8083/connectors/outbox-connector/config", 50 | new StringContent( 51 | File.ReadAllText("debezium-outbox-config.json"), 52 | Encoding.UTF8, 53 | MediaTypeNames.Application.Json) 54 | ); 55 | 56 | if (response.IsSuccessStatusCode) 57 | { 58 | _logger.LogInformation( 59 | "Debezium outbox configured. Status code: {statusCode} Response: {response}", 60 | response.StatusCode, await response.Content.ReadAsStringAsync()); 61 | } 62 | else 63 | { 64 | _logger.LogError("Failed to configure Debezium outbox. Status code: {statusCode}", 65 | response.StatusCode); 66 | throw new Exception("Failed to configure Debezium outbox."); 67 | } 68 | }); 69 | } 70 | } 71 | 72 | public Task StopAsync(CancellationToken cancellationToken) 73 | { 74 | // no-op 75 | return Task.CompletedTask; 76 | } 77 | } -------------------------------------------------------------------------------- /src/Producer/Worker.cs: -------------------------------------------------------------------------------- 1 | using Bogus; 2 | using Events; 3 | using Producer.Data; 4 | 5 | namespace Producer; 6 | 7 | public class Worker : BackgroundService 8 | { 9 | private readonly Faker _faker; 10 | private readonly IServiceScopeFactory _scopeFactory; 11 | private readonly ILogger _logger; 12 | 13 | public Worker(IServiceScopeFactory scopeFactory, ILogger logger) 14 | { 15 | _faker = new Faker(); 16 | _scopeFactory = scopeFactory; 17 | _logger = logger; 18 | } 19 | 20 | protected override async Task ExecuteAsync(CancellationToken stoppingToken) 21 | { 22 | while (!stoppingToken.IsCancellationRequested) 23 | { 24 | _logger.LogInformation("Producing events..."); 25 | 26 | await using var scope = _scopeFactory.CreateAsyncScope(); 27 | await using var db = scope.ServiceProvider.GetRequiredService(); 28 | 29 | await db.OutboxMessages.AddRangeAsync( 30 | GenerateSampleEvents(), 31 | stoppingToken); 32 | 33 | await db.SaveChangesAsync(stoppingToken); 34 | 35 | _logger.LogInformation("Sleeping for a bit"); 36 | 37 | await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken); 38 | } 39 | } 40 | 41 | private IEnumerable GenerateSampleEvents() 42 | => Enumerable 43 | .Range(0, 1000) 44 | .Select(i => 45 | { 46 | if (i % 3 == 0) 47 | { 48 | return new OutboxMessage( 49 | new OrderDelivered( 50 | _faker.Random.Guid(), 51 | DateTime.UtcNow, 52 | _faker.Random.Guid(), 53 | _faker.Random.Guid(), 54 | _faker.Random.ReplaceNumbers("#########")), 55 | _faker.Random.Guid().ToString(), 56 | "order"); 57 | } 58 | 59 | if (i % 5 == 0) 60 | { 61 | return new OutboxMessage( 62 | new OrderCancelled( 63 | _faker.Random.Guid(), 64 | DateTime.UtcNow, 65 | _faker.Random.Guid(), 66 | _faker.Random.Guid(), 67 | _faker.Random.ReplaceNumbers("#########"), 68 | _faker.Rant.Review()), 69 | _faker.Random.Guid().ToString(), 70 | "order"); 71 | } 72 | 73 | return new OutboxMessage( 74 | new OrderCreated( 75 | _faker.Random.Guid(), 76 | DateTime.UtcNow, 77 | _faker.Random.Guid(), 78 | _faker.Random.Guid(), 79 | _faker.Random.ReplaceNumbers("#########")), 80 | _faker.Random.Guid().ToString(), 81 | "order"); 82 | }); 83 | } -------------------------------------------------------------------------------- /src/Events/Kafka/KafkaEventConsumer.cs: -------------------------------------------------------------------------------- 1 | using Confluent.Kafka; 2 | using Microsoft.Extensions.Logging; 3 | 4 | namespace Events.Kafka; 5 | 6 | public class KafkaEventConsumer : IEventConsumer, IDisposable 7 | { 8 | private readonly ILogger _logger; 9 | private readonly IConsumer _consumer; 10 | 11 | public KafkaEventConsumer( 12 | ILogger logger, 13 | JsonEventSerializer serializer, 14 | KafkaOrderEventConsumerSettings settings) 15 | { 16 | _logger = logger; 17 | 18 | var conf = new ConsumerConfig 19 | { 20 | GroupId = settings.ConsumerGroup, 21 | BootstrapServers = "broker:9092", 22 | AutoOffsetReset = AutoOffsetReset.Earliest, 23 | EnableAutoCommit = false 24 | }; 25 | 26 | _consumer = new ConsumerBuilder(conf) 27 | .SetKeyDeserializer(Deserializers.Utf8) 28 | .SetValueDeserializer(serializer) 29 | .Build(); 30 | } 31 | 32 | public Task Subscribe(Action callback, CancellationToken ct) 33 | { 34 | _logger.LogInformation("Subscribing"); 35 | _consumer.Subscribe("order.events"); 36 | 37 | var tcs = new TaskCompletionSource(); 38 | 39 | // polling for messages is a blocking operation, 40 | // so spawning a new thread to keep doing it in the background 41 | var thread = new Thread(() => 42 | { 43 | while (!ct.IsCancellationRequested) 44 | { 45 | try 46 | { 47 | _logger.LogInformation("Waiting for message..."); 48 | 49 | var message = _consumer.Consume(ct); 50 | 51 | _logger.LogInformation( 52 | "Received event {eventId}, of type {eventType}!\n{event}", 53 | message.Message.Value.Id, 54 | message.Message.Value.GetType().Name, 55 | message.Message.Value); 56 | 57 | callback(message.Message.Value); 58 | 59 | _consumer.Commit(); // note: committing every time can have a negative impact on performance 60 | } 61 | catch (OperationCanceledException) when (ct.IsCancellationRequested) 62 | { 63 | _logger.LogInformation("Shutting down gracefully."); 64 | } 65 | catch (Exception ex) 66 | { 67 | // TODO: implement error handling/retry logic 68 | // like this, the failed message will eventually be "marked as processed" 69 | // (commit to a newer offset) even though it failed 70 | _logger.LogError(ex, "Error occurred when consuming event!"); 71 | } 72 | } 73 | 74 | tcs.SetResult(true); 75 | }) 76 | { 77 | IsBackground = true 78 | }; 79 | 80 | thread.Start(); 81 | 82 | return tcs.Task; 83 | } 84 | 85 | public void Dispose() 86 | { 87 | try 88 | { 89 | _consumer?.Close(); 90 | } 91 | catch (Exception) 92 | { 93 | // no exceptions in Dispose :) 94 | } 95 | 96 | _consumer?.Dispose(); 97 | } 98 | } -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | # Kafka stuff from https://github.com/confluentinc/examples/blob/7.0.0-post/cp-all-in-one-community/docker-compose.yml 2 | version: "3" 3 | 4 | services: 5 | zookeeper: 6 | image: confluentinc/cp-zookeeper:7.0.0 7 | hostname: zookeeper 8 | container_name: zookeeper 9 | ports: 10 | - "2181:2181" 11 | environment: 12 | ZOOKEEPER_CLIENT_PORT: 2181 13 | ZOOKEEPER_TICK_TIME: 2000 14 | 15 | broker: 16 | image: confluentinc/cp-kafka:7.0.0 17 | hostname: broker 18 | container_name: broker 19 | depends_on: 20 | - zookeeper 21 | ports: 22 | - "29092:29092" 23 | - "9092:9092" 24 | environment: 25 | KAFKA_BROKER_ID: 1 26 | KAFKA_ZOOKEEPER_CONNECT: 'zookeeper:2181' 27 | KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT 28 | KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://broker:9092,PLAINTEXT_HOST://localhost:29092 29 | KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 30 | KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0 31 | 32 | control-center: 33 | image: confluentinc/cp-enterprise-control-center:7.0.0 34 | hostname: control-center 35 | container_name: control-center 36 | depends_on: 37 | - zookeeper 38 | - broker 39 | #- schema-registry 40 | ports: 41 | - "9021:9021" 42 | environment: 43 | CONTROL_CENTER_BOOTSTRAP_SERVERS: 'broker:9092' 44 | CONTROL_CENTER_ZOOKEEPER_CONNECT: 'zookeeper:32181' 45 | #CONTROL_CENTER_KSQL_URL: "http://ksql-server:8088" 46 | #CONTROL_CENTER_KSQL_ADVERTISED_URL: "http://localhost:8088" 47 | #CONTROL_CENTER_SCHEMA_REGISTRY_URL: "http://schema-registry:8081" 48 | CONTROL_CENTER_REPLICATION_FACTOR: 1 49 | CONTROL_CENTER_INTERNAL_TOPICS_PARTITIONS: 1 50 | CONTROL_CENTER_MONITORING_INTERCEPTOR_TOPIC_PARTITIONS: 1 51 | CONFLUENT_METRICS_TOPIC_REPLICATION: 1 52 | PORT: 9021 53 | 54 | postgres: 55 | image: postgres 56 | container_name: postgres 57 | ports: 58 | - "5432:5432" 59 | environment: 60 | POSTGRES_USER: "user" 61 | POSTGRES_PASSWORD: "pass" 62 | command: [ "postgres", "-c", "wal_level=logical" ] 63 | connect: 64 | image: "debezium/connect:2.1" 65 | container_name: connect 66 | ports: 67 | - 8083:8083 68 | depends_on: 69 | - postgres 70 | - broker 71 | environment: 72 | BOOTSTRAP_SERVERS: broker:9092 73 | GROUP_ID: 1 74 | CONFIG_STORAGE_TOPIC: CONNECT_CONFIGS 75 | OFFSET_STORAGE_TOPIC: CONNECT_OFFSETS 76 | STATUS_STORAGE_TOPIC: CONNECT_STATUSES 77 | seq: 78 | image: "datalust/seq:2021" 79 | hostname: seq 80 | container_name: seq 81 | ports: 82 | - "5341:5341" # ingestion API 83 | - "5555:80" # ui 84 | environment: 85 | ACCEPT_EULA: "Y" 86 | producer: 87 | build: 88 | context: . 89 | dockerfile: ./src/producer/Dockerfile 90 | image: debeziumoutboxsample/producer:latest 91 | container_name: producer 92 | depends_on: 93 | - postgres 94 | consumer: 95 | build: 96 | context: . 97 | dockerfile: ./src/Consumer/Dockerfile 98 | image: debeziumoutboxsample/consumer:latest 99 | container_name: consumer 100 | depends_on: 101 | - broker -------------------------------------------------------------------------------- /DebeziumOutboxSample.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.26124.0 5 | MinimumVisualStudioVersion = 15.0.26124.0 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{41825ACD-D93D-491C-9FD2-7C5D7B849DA6}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Producer", "src\Producer\Producer.csproj", "{4C57CCF9-6D90-4E03-81A5-EEA012607F27}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Events", "src\Events\Events.csproj", "{87DA9D50-B1C6-4B50-BECD-4F8A4A3BDC90}" 11 | EndProject 12 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Consumer", "src\Consumer\Consumer.csproj", "{455315CC-F106-49E6-B473-8A5D59A0FEAB}" 13 | EndProject 14 | Global 15 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 16 | Debug|Any CPU = Debug|Any CPU 17 | Debug|x64 = Debug|x64 18 | Debug|x86 = Debug|x86 19 | Release|Any CPU = Release|Any CPU 20 | Release|x64 = Release|x64 21 | Release|x86 = Release|x86 22 | EndGlobalSection 23 | GlobalSection(SolutionProperties) = preSolution 24 | HideSolutionNode = FALSE 25 | EndGlobalSection 26 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 27 | {4C57CCF9-6D90-4E03-81A5-EEA012607F27}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 28 | {4C57CCF9-6D90-4E03-81A5-EEA012607F27}.Debug|Any CPU.Build.0 = Debug|Any CPU 29 | {4C57CCF9-6D90-4E03-81A5-EEA012607F27}.Debug|x64.ActiveCfg = Debug|Any CPU 30 | {4C57CCF9-6D90-4E03-81A5-EEA012607F27}.Debug|x64.Build.0 = Debug|Any CPU 31 | {4C57CCF9-6D90-4E03-81A5-EEA012607F27}.Debug|x86.ActiveCfg = Debug|Any CPU 32 | {4C57CCF9-6D90-4E03-81A5-EEA012607F27}.Debug|x86.Build.0 = Debug|Any CPU 33 | {4C57CCF9-6D90-4E03-81A5-EEA012607F27}.Release|Any CPU.ActiveCfg = Release|Any CPU 34 | {4C57CCF9-6D90-4E03-81A5-EEA012607F27}.Release|Any CPU.Build.0 = Release|Any CPU 35 | {4C57CCF9-6D90-4E03-81A5-EEA012607F27}.Release|x64.ActiveCfg = Release|Any CPU 36 | {4C57CCF9-6D90-4E03-81A5-EEA012607F27}.Release|x64.Build.0 = Release|Any CPU 37 | {4C57CCF9-6D90-4E03-81A5-EEA012607F27}.Release|x86.ActiveCfg = Release|Any CPU 38 | {4C57CCF9-6D90-4E03-81A5-EEA012607F27}.Release|x86.Build.0 = Release|Any CPU 39 | {87DA9D50-B1C6-4B50-BECD-4F8A4A3BDC90}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 40 | {87DA9D50-B1C6-4B50-BECD-4F8A4A3BDC90}.Debug|Any CPU.Build.0 = Debug|Any CPU 41 | {87DA9D50-B1C6-4B50-BECD-4F8A4A3BDC90}.Debug|x64.ActiveCfg = Debug|Any CPU 42 | {87DA9D50-B1C6-4B50-BECD-4F8A4A3BDC90}.Debug|x64.Build.0 = Debug|Any CPU 43 | {87DA9D50-B1C6-4B50-BECD-4F8A4A3BDC90}.Debug|x86.ActiveCfg = Debug|Any CPU 44 | {87DA9D50-B1C6-4B50-BECD-4F8A4A3BDC90}.Debug|x86.Build.0 = Debug|Any CPU 45 | {87DA9D50-B1C6-4B50-BECD-4F8A4A3BDC90}.Release|Any CPU.ActiveCfg = Release|Any CPU 46 | {87DA9D50-B1C6-4B50-BECD-4F8A4A3BDC90}.Release|Any CPU.Build.0 = Release|Any CPU 47 | {87DA9D50-B1C6-4B50-BECD-4F8A4A3BDC90}.Release|x64.ActiveCfg = Release|Any CPU 48 | {87DA9D50-B1C6-4B50-BECD-4F8A4A3BDC90}.Release|x64.Build.0 = Release|Any CPU 49 | {87DA9D50-B1C6-4B50-BECD-4F8A4A3BDC90}.Release|x86.ActiveCfg = Release|Any CPU 50 | {87DA9D50-B1C6-4B50-BECD-4F8A4A3BDC90}.Release|x86.Build.0 = Release|Any CPU 51 | {455315CC-F106-49E6-B473-8A5D59A0FEAB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 52 | {455315CC-F106-49E6-B473-8A5D59A0FEAB}.Debug|Any CPU.Build.0 = Debug|Any CPU 53 | {455315CC-F106-49E6-B473-8A5D59A0FEAB}.Debug|x64.ActiveCfg = Debug|Any CPU 54 | {455315CC-F106-49E6-B473-8A5D59A0FEAB}.Debug|x64.Build.0 = Debug|Any CPU 55 | {455315CC-F106-49E6-B473-8A5D59A0FEAB}.Debug|x86.ActiveCfg = Debug|Any CPU 56 | {455315CC-F106-49E6-B473-8A5D59A0FEAB}.Debug|x86.Build.0 = Debug|Any CPU 57 | {455315CC-F106-49E6-B473-8A5D59A0FEAB}.Release|Any CPU.ActiveCfg = Release|Any CPU 58 | {455315CC-F106-49E6-B473-8A5D59A0FEAB}.Release|Any CPU.Build.0 = Release|Any CPU 59 | {455315CC-F106-49E6-B473-8A5D59A0FEAB}.Release|x64.ActiveCfg = Release|Any CPU 60 | {455315CC-F106-49E6-B473-8A5D59A0FEAB}.Release|x64.Build.0 = Release|Any CPU 61 | {455315CC-F106-49E6-B473-8A5D59A0FEAB}.Release|x86.ActiveCfg = Release|Any CPU 62 | {455315CC-F106-49E6-B473-8A5D59A0FEAB}.Release|x86.Build.0 = Release|Any CPU 63 | EndGlobalSection 64 | GlobalSection(NestedProjects) = preSolution 65 | {4C57CCF9-6D90-4E03-81A5-EEA012607F27} = {41825ACD-D93D-491C-9FD2-7C5D7B849DA6} 66 | {87DA9D50-B1C6-4B50-BECD-4F8A4A3BDC90} = {41825ACD-D93D-491C-9FD2-7C5D7B849DA6} 67 | {455315CC-F106-49E6-B473-8A5D59A0FEAB} = {41825ACD-D93D-491C-9FD2-7C5D7B849DA6} 68 | EndGlobalSection 69 | EndGlobal 70 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | 12 | # User-specific files (MonoDevelop/Xamarin Studio) 13 | *.userprefs 14 | 15 | # Build results 16 | [Dd]ebug/ 17 | [Dd]ebugPublic/ 18 | [Rr]elease/ 19 | [Rr]eleases/ 20 | x64/ 21 | x86/ 22 | bld/ 23 | [Bb]in/ 24 | [Oo]bj/ 25 | [Ll]og/ 26 | 27 | # Visual Studio 2015/2017 cache/options directory 28 | .vs/ 29 | # Uncomment if you have tasks that create the project's static files in wwwroot 30 | #wwwroot/ 31 | 32 | # Visual Studio 2017 auto generated files 33 | Generated\ Files/ 34 | 35 | # MSTest test Results 36 | [Tt]est[Rr]esult*/ 37 | [Bb]uild[Ll]og.* 38 | 39 | # NUNIT 40 | *.VisualState.xml 41 | TestResult.xml 42 | 43 | # Build Results of an ATL Project 44 | [Dd]ebugPS/ 45 | [Rr]eleasePS/ 46 | dlldata.c 47 | 48 | # Benchmark Results 49 | BenchmarkDotNet.Artifacts/ 50 | 51 | # .NET Core 52 | project.lock.json 53 | project.fragment.lock.json 54 | artifacts/ 55 | 56 | # StyleCop 57 | StyleCopReport.xml 58 | 59 | # Files built by Visual Studio 60 | *_i.c 61 | *_p.c 62 | *_i.h 63 | *.ilk 64 | *.meta 65 | *.obj 66 | *.pch 67 | *.pdb 68 | *.pgc 69 | *.pgd 70 | *.rsp 71 | *.sbr 72 | *.tlb 73 | *.tli 74 | *.tlh 75 | *.tmp 76 | *.tmp_proj 77 | *.log 78 | *.vspscc 79 | *.vssscc 80 | .builds 81 | *.pidb 82 | *.svclog 83 | *.scc 84 | 85 | # Chutzpah Test files 86 | _Chutzpah* 87 | 88 | # Visual C++ cache files 89 | ipch/ 90 | *.aps 91 | *.ncb 92 | *.opendb 93 | *.opensdf 94 | *.sdf 95 | *.cachefile 96 | *.VC.db 97 | *.VC.VC.opendb 98 | 99 | # Visual Studio profiler 100 | *.psess 101 | *.vsp 102 | *.vspx 103 | *.sap 104 | 105 | # Visual Studio Trace Files 106 | *.e2e 107 | 108 | # TFS 2012 Local Workspace 109 | $tf/ 110 | 111 | # Guidance Automation Toolkit 112 | *.gpState 113 | 114 | # ReSharper is a .NET coding add-in 115 | _ReSharper*/ 116 | *.[Rr]e[Ss]harper 117 | *.DotSettings.user 118 | 119 | # JustCode is a .NET coding add-in 120 | .JustCode 121 | 122 | # TeamCity is a build add-in 123 | _TeamCity* 124 | 125 | # DotCover is a Code Coverage Tool 126 | *.dotCover 127 | 128 | # AxoCover is a Code Coverage Tool 129 | .axoCover/* 130 | !.axoCover/settings.json 131 | 132 | # Visual Studio code coverage results 133 | *.coverage 134 | *.coveragexml 135 | 136 | # NCrunch 137 | _NCrunch_* 138 | .*crunch*.local.xml 139 | nCrunchTemp_* 140 | 141 | # MightyMoose 142 | *.mm.* 143 | AutoTest.Net/ 144 | 145 | # Web workbench (sass) 146 | .sass-cache/ 147 | 148 | # Installshield output folder 149 | [Ee]xpress/ 150 | 151 | # DocProject is a documentation generator add-in 152 | DocProject/buildhelp/ 153 | DocProject/Help/*.HxT 154 | DocProject/Help/*.HxC 155 | DocProject/Help/*.hhc 156 | DocProject/Help/*.hhk 157 | DocProject/Help/*.hhp 158 | DocProject/Help/Html2 159 | DocProject/Help/html 160 | 161 | # Click-Once directory 162 | publish/ 163 | 164 | # Publish Web Output 165 | *.[Pp]ublish.xml 166 | *.azurePubxml 167 | # Note: Comment the next line if you want to checkin your web deploy settings, 168 | # but database connection strings (with potential passwords) will be unencrypted 169 | *.pubxml 170 | *.publishproj 171 | 172 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 173 | # checkin your Azure Web App publish settings, but sensitive information contained 174 | # in these scripts will be unencrypted 175 | PublishScripts/ 176 | 177 | # NuGet Packages 178 | *.nupkg 179 | # The packages folder can be ignored because of Package Restore 180 | **/[Pp]ackages/* 181 | # except build/, which is used as an MSBuild target. 182 | !**/[Pp]ackages/build/ 183 | # Uncomment if necessary however generally it will be regenerated when needed 184 | #!**/[Pp]ackages/repositories.config 185 | # NuGet v3's project.json files produces more ignorable files 186 | *.nuget.props 187 | *.nuget.targets 188 | 189 | # Microsoft Azure Build Output 190 | csx/ 191 | *.build.csdef 192 | 193 | # Microsoft Azure Emulator 194 | ecf/ 195 | rcf/ 196 | 197 | # Windows Store app package directories and files 198 | AppPackages/ 199 | BundleArtifacts/ 200 | Package.StoreAssociation.xml 201 | _pkginfo.txt 202 | *.appx 203 | 204 | # Visual Studio cache files 205 | # files ending in .cache can be ignored 206 | *.[Cc]ache 207 | # but keep track of directories ending in .cache 208 | !*.[Cc]ache/ 209 | 210 | # Others 211 | ClientBin/ 212 | ~$* 213 | *~ 214 | *.dbmdl 215 | *.dbproj.schemaview 216 | *.jfm 217 | *.pfx 218 | *.publishsettings 219 | orleans.codegen.cs 220 | 221 | # Including strong name files can present a security risk 222 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 223 | #*.snk 224 | 225 | # Since there are multiple workflows, uncomment next line to ignore bower_components 226 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 227 | #bower_components/ 228 | 229 | # RIA/Silverlight projects 230 | Generated_Code/ 231 | 232 | # Backup & report files from converting an old project file 233 | # to a newer Visual Studio version. Backup files are not needed, 234 | # because we have git ;-) 235 | _UpgradeReport_Files/ 236 | Backup*/ 237 | UpgradeLog*.XML 238 | UpgradeLog*.htm 239 | 240 | # SQL Server files 241 | *.mdf 242 | *.ldf 243 | *.ndf 244 | 245 | # Business Intelligence projects 246 | *.rdl.data 247 | *.bim.layout 248 | *.bim_*.settings 249 | 250 | # Microsoft Fakes 251 | FakesAssemblies/ 252 | 253 | # GhostDoc plugin setting file 254 | *.GhostDoc.xml 255 | 256 | # Node.js Tools for Visual Studio 257 | .ntvs_analysis.dat 258 | node_modules/ 259 | 260 | # TypeScript v1 declaration files 261 | typings/ 262 | 263 | # Visual Studio 6 build log 264 | *.plg 265 | 266 | # Visual Studio 6 workspace options file 267 | *.opt 268 | 269 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 270 | *.vbw 271 | 272 | # Visual Studio LightSwitch build output 273 | **/*.HTMLClient/GeneratedArtifacts 274 | **/*.DesktopClient/GeneratedArtifacts 275 | **/*.DesktopClient/ModelManifest.xml 276 | **/*.Server/GeneratedArtifacts 277 | **/*.Server/ModelManifest.xml 278 | _Pvt_Extensions 279 | 280 | # Paket dependency manager 281 | .paket/paket.exe 282 | paket-files/ 283 | 284 | # FAKE - F# Make 285 | .fake/ 286 | 287 | # JetBrains Rider 288 | .idea/ 289 | *.sln.iml 290 | 291 | # CodeRush 292 | .cr/ 293 | 294 | # Python Tools for Visual Studio (PTVS) 295 | __pycache__/ 296 | *.pyc 297 | 298 | # Cake - Uncomment if you are using it 299 | # tools/** 300 | # !tools/packages.config 301 | 302 | # Tabs Studio 303 | *.tss 304 | 305 | # Telerik's JustMock configuration file 306 | *.jmconfig 307 | 308 | # BizTalk build output 309 | *.btp.cs 310 | *.btm.cs 311 | *.odx.cs 312 | *.xsd.cs 313 | 314 | # OpenCover UI analysis results 315 | OpenCover/ 316 | 317 | # Azure Stream Analytics local run output 318 | ASALocalRun/ 319 | 320 | # MSBuild Binary and Structured Log 321 | *.binlog 322 | 323 | #MacOS stuff 324 | .DS_Store 325 | 326 | tools/ -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # for details, check https://docs.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/ 2 | # inspired by: 3 | # - https://github.com/dotnet/runtime/blob/main/.editorconfig 4 | # - https://github.com/dotnet/aspnetcore/blob/main/.editorconfig 5 | 6 | # top-most EditorConfig file 7 | root = true 8 | 9 | [*.cs] 10 | 11 | # Don't use this. qualifier 12 | dotnet_style_qualification_for_field = false:suggestion 13 | dotnet_style_qualification_for_property = false:suggestion 14 | dotnet_style_qualification_for_method = false:suggestion 15 | dotnet_style_qualification_for_event = false:suggestion 16 | 17 | # use int x = .. over Int32 18 | dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion 19 | 20 | # use int.MaxValue over Int32.MaxValue 21 | dotnet_style_predefined_type_for_member_access = true:suggestion 22 | 23 | # prefix private instance fields 24 | dotnet_naming_rule.private_members_with_underscore.symbols = private_fields 25 | dotnet_naming_rule.private_members_with_underscore.style = prefix_underscore 26 | dotnet_naming_rule.private_members_with_underscore.severity = suggestion 27 | 28 | dotnet_naming_symbols.private_fields.applicable_kinds = field 29 | dotnet_naming_symbols.private_fields.applicable_accessibilities = private 30 | 31 | dotnet_naming_style.prefix_underscore.capitalization = camel_case 32 | dotnet_naming_style.prefix_underscore.required_prefix = _ 33 | 34 | # pascal case constants and static readonly 35 | dotnet_naming_style.constant_or_static_readonly.capitalization = pascal_case 36 | 37 | ## const 38 | dotnet_naming_rule.private_const_fields.symbols = private_const_fields 39 | dotnet_naming_rule.private_const_fields.style = constant_or_static_readonly 40 | dotnet_naming_rule.private_const_fields.severity = suggestion 41 | 42 | dotnet_naming_symbols.private_const_fields.applicable_kinds = field 43 | dotnet_naming_symbols.private_const_fields.required_modifiers = const 44 | 45 | ## static readonly 46 | dotnet_naming_rule.private_static_readonly_fields.symbols = private_static_readonly_fields 47 | dotnet_naming_rule.private_static_readonly_fields.style = constant_or_static_readonly 48 | dotnet_naming_rule.private_static_readonly_fields.severity = suggestion 49 | 50 | dotnet_naming_symbols.private_static_readonly_fields.applicable_kinds = field 51 | dotnet_naming_symbols.private_static_readonly_fields.required_modifiers = static, readonly 52 | 53 | # use expression bodied members when there's a single expression 54 | csharp_style_expression_bodied_constructors = true:suggestion 55 | csharp_style_expression_bodied_methods = true:suggestion 56 | csharp_style_expression_bodied_operators = true:suggestion 57 | csharp_style_expression_bodied_properties = true:suggestion 58 | csharp_style_expression_bodied_indexers = true:suggestion 59 | csharp_style_expression_bodied_accessors = true:suggestion 60 | csharp_style_expression_bodied_lambdas = true:suggestion 61 | csharp_style_expression_bodied_local_functions = true:suggestion 62 | 63 | # misc 64 | csharp_prefer_static_local_function = true:suggestion 65 | csharp_using_directive_placement = outside_namespace:suggestion 66 | csharp_prefer_braces = true:suggestion 67 | csharp_prefer_static_local_function = false:suggestion 68 | csharp_style_prefer_switch_expression = true:suggestion 69 | csharp_style_namespace_declarations = file_scoped 70 | dotnet_style_readonly_field = true:suggestion 71 | dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion 72 | 73 | # CA1047: Do not declare protected member in sealed type 74 | dotnet_diagnostic.CA1047.severity = warning 75 | 76 | # CA1507: Use nameof to express symbol names 77 | dotnet_diagnostic.CA1507.severity = warning 78 | 79 | # CA1725: Parameter names should match base declaration 80 | dotnet_diagnostic.CA1725.severity = suggestion 81 | 82 | # CA1802: Use literals where appropriate 83 | dotnet_diagnostic.CA1802.severity = warning 84 | 85 | # CA1821: Remove empty Finalizers 86 | dotnet_diagnostic.CA1821.severity = warning 87 | 88 | # CA1822: Make member static 89 | dotnet_diagnostic.CA1822.severity = suggestion 90 | 91 | # CA1823: Avoid unused private fields 92 | dotnet_diagnostic.CA1823.severity = warning 93 | 94 | # CA1825: Avoid zero-length array allocations 95 | dotnet_diagnostic.CA1825.severity = warning 96 | 97 | # CA1826: Do not use Enumerable methods on indexable collections. Instead use the collection directly 98 | dotnet_diagnostic.CA1826.severity = warning 99 | 100 | # CA1827: Do not use Count() or LongCount() when Any() can be used 101 | dotnet_diagnostic.CA1827.severity = warning 102 | 103 | # CA1828: Do not use CountAsync() or LongCountAsync() when AnyAsync() can be used 104 | dotnet_diagnostic.CA1828.severity = warning 105 | 106 | # CA1829: Use Length/Count property instead of Count() when available 107 | dotnet_diagnostic.CA1829.severity = warning 108 | 109 | # CA1830: Prefer strongly-typed Append and Insert method overloads on StringBuilder 110 | dotnet_diagnostic.CA1830.severity = warning 111 | 112 | # CA1831: Use AsSpan or AsMemory instead of Range-based indexers when appropriate 113 | # CA1832: Use AsSpan or AsMemory instead of Range-based indexers when appropriate 114 | # CA1833: Use AsSpan or AsMemory instead of Range-based indexers when appropriate 115 | dotnet_diagnostic.CA1831.severity = warning 116 | dotnet_diagnostic.CA1832.severity = warning 117 | dotnet_diagnostic.CA1833.severity = warning 118 | 119 | # CA1834: Consider using 'StringBuilder.Append(char)' when applicable 120 | dotnet_diagnostic.CA1834.severity = warning 121 | 122 | # CA1835: Prefer the 'Memory'-based overloads for 'ReadAsync' and 'WriteAsync' 123 | dotnet_diagnostic.CA1835.severity = warning 124 | 125 | # CA1836: Prefer IsEmpty over Count 126 | dotnet_diagnostic.CA1836.severity = warning 127 | 128 | # CA1841: Prefer Dictionary.Contains methods 129 | dotnet_diagnostic.CA1841.severity = warning 130 | 131 | # CA1842: Do not use 'WhenAll' with a single task 132 | dotnet_diagnostic.CA1842.severity = warning 133 | 134 | # CA1843: Do not use 'WaitAll' with a single task 135 | dotnet_diagnostic.CA1843.severity = warning 136 | 137 | # CA1845: Use span-based 'string.Concat' 138 | dotnet_diagnostic.CA1845.severity = warning 139 | 140 | # CA1846: Prefer AsSpan over Substring 141 | dotnet_diagnostic.CA1846.severity = warning 142 | 143 | # CA2011: Avoid infinite recursion 144 | dotnet_diagnostic.CA2011.severity = warning 145 | 146 | # CA2012: Use ValueTask correctly 147 | dotnet_diagnostic.CA2012.severity = warning 148 | 149 | # CA2013: Do not use ReferenceEquals with value types 150 | dotnet_diagnostic.CA2013.severity = warning 151 | 152 | # CA2016: Forward the 'CancellationToken' parameter to methods that take one 153 | dotnet_diagnostic.CA2016.severity = warning 154 | 155 | # CA2200: Rethrow to preserve stack details 156 | dotnet_diagnostic.CA2200.severity = warning 157 | 158 | # CA2208: Instantiate argument exceptions correctly 159 | dotnet_diagnostic.CA2208.severity = warning 160 | 161 | # IDE0035: Remove unreachable code 162 | dotnet_diagnostic.IDE0035.severity = warning 163 | 164 | # IDE0036: Order modifiers 165 | csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:suggestion 166 | dotnet_diagnostic.IDE0036.severity = warning 167 | 168 | # IDE0043: Format string contains invalid placeholder 169 | dotnet_diagnostic.IDE0043.severity = warning 170 | --------------------------------------------------------------------------------