Skip to content

Commit 6d8f28b

Browse files
committed
add conveyo
1 parent dd3985a commit 6d8f28b

17 files changed

Lines changed: 69 additions & 75 deletions

File tree

README.md

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,6 @@ In this repository, **MSA** stands for **Message-driven Service Architecture**:
77
[![Release](https://github.com/ftechmax/msa-templates/actions/workflows/release.yml/badge.svg)](https://github.com/ftechmax/msa-templates/actions/workflows/release.yml)
88
[![codecov](https://codecov.io/gh/ftechmax/msa-templates/graph/badge.svg?token=I4QI609IIQ)](https://codecov.io/gh/ftechmax/msa-templates)
99

10-
> [!NOTE]
11-
> The templates currently use **MassTransit**, but this will be removed in a future update and replaced by my own library **Conveyo**: https://github.com/ftechmax/conveyo. Conveyo will also support workers written in Go.
12-
> MassTransit's licensing model changed, and I want the messaging stack to remain fully open and free.
13-
1410
> [!NOTE]
1511
> If you previously installed the now-retired `MSA.Templates` NuGet package, you can remove the stale local install with `dotnet new uninstall MSA.Templates`. The new flow does not use `dotnet new`.
1612
@@ -37,7 +33,7 @@ These templates are the accumulation of those lessons, packaged as a starting po
3733
- A clear split between the **HTTP edge** in the API and **asynchronous domain work** in the worker
3834
- A default stack for **messaging, persistence, caching, and telemetry** picked from what I reach for on every project
3935
- **Kubernetes** manifests included for local, self-hosted, and cloud clusters
40-
- Opinionated defaults you can rip out: every choice (MassTransit, PostgreSQL, Valkey, SignalR) is isolated behind its own wiring so you can swap one without touching the rest
36+
- Opinionated defaults you can rip out: every choice (Conveyo, PostgreSQL, Valkey, SignalR) is isolated behind its own wiring so you can swap one without touching the rest
4137

4238
This is not meant to be the only way to do things. It's the default I've shipped to production more than once.
4339

@@ -54,7 +50,7 @@ These templates are intended to be used together as a "vertical slice" of a mess
5450

5551
The templates come wired up for:
5652

57-
- **Messaging** via MassTransit (Conveyo coming soon!) on top of RabbitMQ
53+
- **Messaging** via Conveyo on top of RabbitMQ
5854
- **Persistence** via the Npgsql package, storing documents as PostgreSQL JSONB (an in-cluster Postgres StatefulSet in the generated Kubernetes setup)
5955
- **Caching** via Valkey using the StackExchange.Redis package
6056
- **Real-time updates** via SignalR

docs/api.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ The API should not contain domain logic. Its main jobs are:
88

99
- **Own the HTTP contract**: controllers, DTOs, a versioning strategy when you need one, and input validation.
1010
- **Validate early**: reject bad requests before they become messages that bounce around the system.
11-
- **Send commands** to the worker via the bus using MassTransit. The API should not need direct DB access to do writes.
11+
- **Send commands** to the worker via the bus using Conveyo. The API should not need direct DB access to do writes.
1212
- **Serve reads efficiently**: the template uses projections and caches them in Valkey/Redis so read paths stay fast without coupling reads to the write model.
1313

1414
The API also bridges worker events back to connected clients. It consumes published events from the worker and uses them to:

docs/worker.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Worker project
22

3-
The worker service is responsible for consuming commands and external events, applying business logic, persisting state, and publishing events that represent domain outcomes. It runs as a background service and does not expose HTTP endpoints. The template wires up MassTransit on RabbitMQ for messaging, Npgsql for persistence (PostgreSQL JSONB, an in-cluster Postgres StatefulSet in the default Kubernetes setup), and OpenTelemetry for traces, metrics, and logs.
3+
The worker service is responsible for consuming commands and external events, applying business logic, persisting state, and publishing events that represent domain outcomes. It runs as a background service and does not expose HTTP endpoints. The template wires up Conveyo on RabbitMQ for messaging, Npgsql for persistence (PostgreSQL JSONB, an in-cluster Postgres StatefulSet in the default Kubernetes setup), and OpenTelemetry for traces, metrics, and logs.
44

55
## Responsibilities
66

@@ -16,7 +16,7 @@ In this stack, the worker owns the domain work:
1616

1717
- Handlers must be **idempotent** because RabbitMQ can redeliver the same message after a worker restart or ack timeout.
1818
- OpenTelemetry exports traces, metrics, and logs, so you can answer "what happened to message X?" from a single trace ID.
19-
- Transient failures go through MassTransit's retry pipeline, and unrecoverable ones land in `_error` queues. The template ships defaults; you set the retry counts and backoff per consumer.
19+
- Transient failures go through Conveyo's retry pipeline (exponential backoff), and unrecoverable ones are published as a `Fault` and moved to a `{queue}_error` queue. The template ships defaults; you set the retry counts and backoff per consumer.
2020
- Background jobs belong here too. The template shows a hosted service for cache invalidation.
2121

2222
## Command handling and event publication

templates/api/ApplicationName.Api.Application.Test/Services/ExampleServiceTest.cs

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
using FakeItEasy;
1010
using Mapster;
1111
using MapsterMapper;
12-
using MassTransit;
12+
using Conveyo;
1313
using NUnit.Framework;
1414
using Shouldly;
1515

@@ -25,8 +25,6 @@ public class ExampleServiceTest
2525

2626
private IBus _bus;
2727

28-
private ISendEndpoint _sendEndpoint;
29-
3028
private ExampleService _subjectUnderTest;
3129

3230
[SetUp]
@@ -36,13 +34,7 @@ public void Setup()
3634

3735
_protoCacheRepository = _fixture.Freeze<IProtoCacheRepository>();
3836

39-
var queue = new Uri("queue:example");
40-
EndpointConvention.Map<CreateExampleCommand>(queue);
41-
EndpointConvention.Map<UpdateExampleCommand>(queue);
42-
4337
_bus = _fixture.Freeze<IBus>();
44-
_sendEndpoint = _fixture.Freeze<ISendEndpoint>();
45-
A.CallTo(() => _bus.GetSendEndpoint(A<Uri>._)).Returns(_sendEndpoint);
4638

4739
var mapperConfig = new TypeAdapterConfig();
4840
mapperConfig.Scan(typeof(MappingProfile).Assembly);
@@ -220,7 +212,7 @@ public async Task HandleAsync_CreateExampleDto()
220212
var dto = _fixture.Create<CreateExampleDto>();
221213

222214
var capturedCommand = default(CreateExampleCommand);
223-
A.CallTo(() => _sendEndpoint.Send(A<CreateExampleCommand>._, A<CancellationToken>._)).Invokes(
215+
A.CallTo(() => _bus.Send(A<CreateExampleCommand>._, A<CancellationToken>._)).Invokes(
224216
(CreateExampleCommand arg0, CancellationToken _) =>
225217
{
226218
capturedCommand = arg0;
@@ -230,7 +222,7 @@ public async Task HandleAsync_CreateExampleDto()
230222
await _subjectUnderTest.HandleAsync(dto);
231223

232224
// Assert
233-
A.CallTo(() => _sendEndpoint.Send(A<CreateExampleCommand>._, A<CancellationToken>._)).MustHaveHappenedOnceExactly();
225+
A.CallTo(() => _bus.Send(A<CreateExampleCommand>._, A<CancellationToken>._)).MustHaveHappenedOnceExactly();
234226

235227
capturedCommand.ShouldNotBeNull();
236228
capturedCommand.Name.ShouldSatisfyAllConditions(
@@ -246,7 +238,7 @@ public async Task HandleAsync_UpdateExampleDto()
246238
var dto = _fixture.Create<UpdateExampleDto>();
247239

248240
var capturedCommand = default(UpdateExampleCommand);
249-
A.CallTo(() => _sendEndpoint.Send(A<UpdateExampleCommand>._, A<CancellationToken>._)).Invokes(
241+
A.CallTo(() => _bus.Send(A<UpdateExampleCommand>._, A<CancellationToken>._)).Invokes(
250242
(UpdateExampleCommand arg0, CancellationToken _) =>
251243
{
252244
capturedCommand = arg0;
@@ -256,7 +248,7 @@ public async Task HandleAsync_UpdateExampleDto()
256248
await _subjectUnderTest.HandleAsync(id, dto);
257249

258250
// Assert
259-
A.CallTo(() => _sendEndpoint.Send(A<UpdateExampleCommand>._, A<CancellationToken>._)).MustHaveHappenedOnceExactly();
251+
A.CallTo(() => _bus.Send(A<UpdateExampleCommand>._, A<CancellationToken>._)).MustHaveHappenedOnceExactly();
260252

261253
capturedCommand.ShouldNotBeNull();
262254
capturedCommand.CorrelationId.ShouldSatisfyAllConditions(

templates/api/ApplicationName.Api.Application/ApplicationName.Api.Application.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
<ItemGroup>
1010
<PackageReference Include="ArgDefender" Version="2.1.0" />
1111
<PackageReference Include="Mapster.DependencyInjection" Version="10.0.7" />
12-
<PackageReference Include="MassTransit" Version="8.5.7" />
12+
<PackageReference Include="Conveyo" Version="0.1.0-preview.*" />
1313
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="10.0.8" />
1414
<PackageReference Include="protobuf-net" Version="3.2.56" />
1515
</ItemGroup>

templates/api/ApplicationName.Api.Application/Services/ExampleService.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
using ApplicationName.Shared.Projections;
66
using ArgDefender;
77
using MapsterMapper;
8-
using MassTransit;
8+
using Conveyo;
99

1010
namespace ApplicationName.Api.Application.Services;
1111

templates/api/ApplicationName.Api.Test/Consumers/LocalEventHandlerTest.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
using AutoFixture;
66
using AutoFixture.AutoFakeItEasy;
77
using FakeItEasy;
8-
using MassTransit;
8+
using Conveyo;
99
using NUnit.Framework;
1010

1111
namespace ApplicationName.Api.Test.Consumers

templates/api/ApplicationName.Api/ApplicationName.Api.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@
1313
<PackageReference Include="FluentValidation" Version="12.1.1" />
1414
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="12.1.1" />
1515
<PackageReference Include="Mapster" Version="10.0.7" />
16-
<PackageReference Include="MassTransit" Version="8.5.9" />
17-
<PackageReference Include="MassTransit.RabbitMQ" Version="8.5.9" />
16+
<PackageReference Include="Conveyo" Version="0.1.0-preview.*" />
17+
<PackageReference Include="Conveyo.RabbitMQ" Version="0.1.0-preview.*" />
1818
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="10.0.8" />
1919
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.23.0" />
2020
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" Version="1.15.1" />

templates/api/ApplicationName.Api/Consumers/LocalEventHandler.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
using ApplicationName.Api.Contracts;
44
using ApplicationName.Shared.Commands;
55
using ApplicationName.Shared.Events;
6-
using MassTransit;
6+
using Conveyo;
77
using Microsoft.AspNetCore.SignalR;
88

99
namespace ApplicationName.Api.Consumers;

templates/api/ApplicationName.Api/Program.cs

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@
77
using ApplicationName.Api.Infrastructure;
88
using ApplicationName.Api.Validators;
99
using ApplicationName.Shared.Commands;
10+
using ApplicationName.Shared.Events;
1011
using FluentValidation;
1112
using Mapster;
12-
using MassTransit;
13-
using MassTransit.Logging;
13+
using Conveyo;
14+
using Conveyo.Diagnostics;
15+
using Conveyo.RabbitMQ;
1416
using OpenTelemetry.Logs;
1517
using OpenTelemetry.Metrics;
1618
using OpenTelemetry.Resources;
@@ -56,18 +58,25 @@ private static void ConfigureServices(IServiceCollection services, Configuration
5658
services.AddMapster();
5759
TypeAdapterConfig.GlobalSettings.Scan(Assembly.GetExecutingAssembly());
5860

59-
// MassTransit + RabbitMQ
60-
services.AddMassTransit(i =>
61+
// Conveyo + RabbitMQ
62+
services.AddConveyo(bus =>
6163
{
64+
// Message type URNs, must match the worker mappings
65+
bus.Map<CreateExampleCommand>("applicationname:CreateExampleCommand.v1");
66+
bus.Map<UpdateExampleCommand>("applicationname:UpdateExampleCommand.v1");
67+
bus.Map<ExampleCreatedEvent>("applicationname:ExampleCreatedEvent.v1");
68+
bus.Map<ExampleUpdatedEvent>("applicationname:ExampleUpdatedEvent.v1");
69+
bus.Map<ExampleRemoteCodeSetEvent>("applicationname:ExampleRemoteCodeSetEvent.v1");
70+
6271
var uri = new Uri("queue:ApplicationName.Worker");
63-
EndpointConvention.Map<CreateExampleCommand>(uri);
64-
EndpointConvention.Map<UpdateExampleCommand>(uri);
72+
bus.MapEndpointConvention<CreateExampleCommand>(uri);
73+
bus.MapEndpointConvention<UpdateExampleCommand>(uri);
6574

66-
i.AddConsumer<LocalEventHandler>();
75+
bus.AddConsumer<LocalEventHandler>();
6776

68-
i.UsingRabbitMq((ctx, cfg) =>
77+
bus.UsingRabbitMq((ctx, cfg) =>
6978
{
70-
cfg.Host(configuration["rabbitmq:host"], configuration["rabbitmq:vhost"], h =>
79+
cfg.Host(configuration["rabbitmq:host"]!, configuration["rabbitmq:vhost"]!, h =>
7180
{
7281
h.Username(configuration["rabbitmq:username"]!);
7382
h.Password(configuration["rabbitmq:password"]!);
@@ -128,7 +137,7 @@ private static void ConfigureServices(IServiceCollection services, Configuration
128137
{
129138
options.RecordException = true;
130139
})
131-
.AddSource(DiagnosticHeaders.DefaultListenerName) // MassTransit
140+
.AddSource(DiagnosticHeaders.DefaultListenerName) // Conveyo
132141
.AddRedisInstrumentation()
133142
.AddOtlpExporter(configure =>
134143
{

0 commit comments

Comments
 (0)