Codebelt

Savvyio.Commands.Messaging

Move Savvyio commands across message-based boundaries without changing the command model.

.NET 10.0 / .NET 9.0 MIT v5.0.8 5,300 downloads

Overview

Savvyio.Commands.Messaging turns ICommand payloads into distributed messages and gives you an in-memory point-to-point queue for command delivery. It sits directly on top of Savvyio.Commands and Savvyio.Messaging, so the command stays the payload while the message adds transport-facing metadata such as identifier, source, type, and time.

The public surface is narrow. It adds one envelope conversion API for commands and one queue implementation that is explicitly described as useful for unit testing and similar in-process scenarios.

Key APIs

CommandExtensions.ToMessage<T> wraps any ICommand in a Message<T> and returns it as IMessage<T>. It requires a non-null command, a non-null Uri source, and a non-empty message type, and it optionally accepts MessageOptions so callers can control MessageId and Time before the message is created.

InMemoryCommandQueue implements IPointToPointChannel<ICommand> with an unbounded Channel<IMessage<ICommand>>. Use it when you want a local sender and receiver pair for command messages without introducing a broker-specific dependency.

InMemoryCommandQueue.SendAsync accepts a sequence of IMessage<ICommand> values and writes each one to the internal channel. The method honors the CancellationToken configured through AsyncOptions.

InMemoryCommandQueue.ReceiveAsync asynchronously yields queued command messages until the reader has no more buffered items. It also uses AsyncOptions, which lets the receive loop participate in cancellation-aware tests or in-process message flows.

Basic usage

using System;
using System.Threading.Tasks;
using Codebelt.Extensions.Xunit;
using Savvyio.Commands;
using Savvyio.Commands.Messaging;
using Savvyio.Messaging;
using Xunit;

namespace MyProject.Tests;

public class CommandMessagingExamples : Test
{
    public CommandMessagingExamples(ITestOutputHelper output) : base(output) { }

    [Fact]
    public async Task WrapCommand_InMemoryQueue_ReceivesEnvelope()
    {
        var queue = new InMemoryCommandQueue();
        var command = new ArchiveMemberCommand("member-42");
        var source = new Uri("https://membership.example.test/commands");
        var queuedAt = new DateTime(2026, 1, 15, 12, 0, 0, DateTimeKind.Utc);
        var outbound = command.ToMessage(source, "members.archive.requested", options =>
        {
            options.MessageId = "archive-member-42";
            options.Time = queuedAt;
        });

        await queue.SendAsync(new IMessage<ICommand>[] { outbound });

        IMessage<ICommand>? received = null;
        var count = 0;
        await foreach (var message in queue.ReceiveAsync())
        {
            received = message;
            count++;
        }

        Assert.NotNull(received);
        var payload = Assert.IsType<ArchiveMemberCommand>(received!.Data);
        Assert.Equal(1, count);
        Assert.Equal("archive-member-42", received.Id);
        Assert.Equal(source.OriginalString, received.Source);
        Assert.Equal("members.archive.requested", received.Type);
        Assert.Equal(queuedAt, received.Time);
        Assert.Equal("member-42", payload.MemberId);
        TestOutput.WriteLine($"{received.Id} queued {payload.MemberId} from {received.Source}.");
    }

    private sealed record ArchiveMemberCommand(string MemberId) : Command;
}

Use this pattern when a local ICommand needs to cross a queue or similar message boundary while keeping the original command type as the payload. It matters because ToMessage adds the envelope metadata that distributed receivers need, and InMemoryCommandQueue lets you exercise that flow without introducing a transport-specific dependency.

Installation

dotnet add package Savvyio.Commands.Messaging

Usage guidance

Choose this package when your application already models work as ICommand implementations and you need to package those commands for point-to-point delivery. If you only need in-process command dispatching, Savvyio.Commands is the better fit, and if you need a real broker integration you should pair this package with the transport-specific Savvyio extension that matches your messaging system.

Family packages