Codebelt

Savvyio.Messaging

Use message envelopes, acknowledgement callbacks, and signature checks when Savvyio requests cross subsystem boundaries.

.NET 10.0 / .NET 9.0 MIT v5.0.8 9,810 downloads

Overview

Savvyio.Messaging turns IRequest payloads from Savvyio.Core into concrete message envelopes for distributed workflows. Message<T> carries an id, source, type, payload, UTC timestamp, and acknowledgement state so the same request model can move through a messaging boundary with its envelope metadata intact.

The package also adds two higher-level behaviors. MessageAsyncEnumerable<T> coordinates asynchronous consumption with acknowledgement aggregation, and the cryptography APIs can serialize a message through an IMarshaller, attach an HMAC signature, and fail verification when the signed envelope no longer matches its calculated hash.

Key APIs

Message<T> is the default message envelope. Its public constructor rejects null or whitespace identifiers and type names, requires a non-null source URI and payload, and either preserves a supplied UTC timestamp or assigns DateTime.UtcNow.

Acknowledgeable is the shared base record for Message<T> and SignedMessage<T>. It owns the mutable Properties bag, exposes AcknowledgeAsync(), and raises the asynchronous Acknowledged event through the protected OnAcknowledgedAsync hook.

MessageAsyncEnumerable<T> wraps either IEnumerable<IMessage<T>> or IAsyncEnumerable<IMessage<T>> and returns an async enumerator that runs message processing through a configured callback while tracking acknowledgements on each message.

MessageAsyncEnumerableOptions<T> defines that processing contract. MessageCallback is required, AcknowledgedProperties defaults to a ConcurrentBag<IDictionary<string, object>>, and AcknowledgedPropertiesCallback receives the collected acknowledgement state after enumeration completes.

SignedMessage<T> is the signed envelope implementation. It copies the original message id, source, type, time, and payload into an acknowledgeable record and adds the Signature value that represents the serialized message content.

Savvyio.Messaging.Cryptography.MessageExtensions.Sign<T> serializes an IMessage<T> with an IMarshaller, hashes the serialized data with KeyedHashFactory.CreateHmacCrypto, and returns an ISignedMessage<T>. The setup delegate must produce a valid SignedMessageOptions instance before signing can proceed.

Savvyio.Messaging.Cryptography.SignedMessageExtensions.CheckSignature<T> verifies a signed envelope by cloning the underlying message data, recalculating the signature, and throwing ArgumentOutOfRangeException when the supplied secret or algorithm does not reproduce the stored signature.

SignedMessageOptions configures signature generation and verification. It defaults SignatureAlgorithm to HmacSha256 and refuses a null or empty SignatureSecret when ValidateOptions() runs.

Basic usage

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Codebelt.Extensions.Xunit;
using Savvyio;
using Savvyio.Messaging;
using Xunit;

namespace MyProject.Tests;

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

    [Fact]
    public async Task Enumerate_AcknowledgedMessage_CapturesProperties()
    {
        var envelope = new Message<OrderSubmitted>("msg-001", new Uri("urn:orders:checkout"), nameof(OrderSubmitted), new OrderSubmitted(Guid.NewGuid()));
        List<IDictionary<string, object>> acknowledged = new();

        var sut = new MessageAsyncEnumerable<OrderSubmitted>(new IMessage<OrderSubmitted>[] { envelope }, options =>
        {
            options.MessageCallback = async message =>
            {
                message.Properties["Stage"] = "validated";
                message.Properties["OrderId"] = message.Data.OrderId;
                await message.AcknowledgeAsync();
            };
            options.AcknowledgedPropertiesCallback = properties =>
            {
                acknowledged = properties.Select(p => (IDictionary<string, object>)new Dictionary<string, object>(p)).ToList();
                return Task.CompletedTask;
            };
        });

        await foreach (var message in sut) { TestOutput.WriteLine($"{message.Id} acknowledged from {message.Source}"); }

        var state = Assert.Single(acknowledged);
        Assert.Equal("validated", (string)state["Stage"]);
        Assert.Equal(envelope.Data.OrderId, (Guid)state["OrderId"]);
    }
}

public sealed record OrderSubmitted(Guid OrderId) : Request;

Use this pattern when a receiver needs to process streamed IMessage<T> envelopes and publish acknowledgement state after each message succeeds. It matters because the package couples async iteration, acknowledgement hooks, and shared message properties without forcing transport-specific code into the consumer loop.

Installation

dotnet add package Savvyio.Messaging

Usage guidance

Use Savvyio.Messaging when an IRequest needs a reusable envelope with explicit message identity, acknowledgement hooks, or cryptographic signing before it crosses process or subsystem boundaries. If you only need in-process request and handler abstractions, Savvyio.Core is the smaller fit, and if you need broker-specific transports or dependency injection wiring, move to the sibling transport and extension packages that build on this envelope model.

Family packages