Codebelt

Savvyio.Core

Core contracts for requests, metadata, handlers, repositories, and transport-neutral messaging across Savvy I/O.

.NET 10.0 / .NET 9.0 MIT v5.0.8 31,156 downloads

Overview

Savvyio.Core is the base package for Savvy I/O's request, metadata, handler, dispatcher, repository, and messaging model. It defines the contracts and abstract building blocks that higher-level packages use for commands, queries, domain events, integration events, aggregate roots, and transport-neutral message flows.

The package stays at the abstraction layer. It gives you request and metadata primitives, handler activators, abstract dispatchers, repository and data-store contracts, message channel interfaces, and assembly-scanning options, but it does not provide concrete persistence engines, transports, or dependency injection integrations by itself.

Key APIs

Request is the abstract record base for package-defined requests. Its constructor creates a MetadataDictionary and immediately stores the runtime member type, which gives every derived request a consistent metadata surface before any handler or message wrapper touches it.

MetadataExtensions adds the common cross-cutting metadata operations for any IMetadata implementation, including causation, correlation, request, and event identifiers, timestamps, member type, arbitrary entries, and metadata merging. Tests verify both the UTC guard on SetTimestamp and the rule that MergeMetadata copies only missing keys.

HandlerFactory creates IFireForgetActivator<TRequest> and IRequestReplyActivator<TRequest> instances from registry callbacks. The factory-backed managers match handlers by the runtime request type, so a handler can expose multiple delegates without hand-writing its own dispatch table.

FireForgetDispatcher is the abstract dispatcher for in-only flows. Its protected Dispatch and DispatchAsync methods resolve all matching handlers through IServiceLocator, allow multiple handlers for the same request type, and throw OrphanedHandlerException when no registered delegate accepts the request.

RequestReplyDispatcher is the abstract dispatcher for request-reply flows. Its protected Dispatch and DispatchAsync methods return the first successful result from the resolved handlers and use the same orphaned-handler guard when no handler can answer the request.

IAggregateRoot<TEvent, TKey> defines the event-backed aggregate contract for DDD models. It combines identity with an Events collection and RemoveAllEvents, while the related ITracedAggregateRoot<TKey> adds the Version member used by event-sourced aggregate variants.

IPersistentRepository<TEntity, TKey> is the repository-side persistence abstraction for domain entities. It composes add, remove, get-by-id, search, and unit-of-work-friendly write patterns, and its data-store counterpart IPersistentDataStore<T, TOptions> offers the same CRUD shape for DTO-oriented access layers.

IMessage<T> is the transport-neutral message contract for distributed boundaries. It combines CloudEvents-style identity members such as Id, Source, Type, Time, and Data with IAcknowledgeable, which adds shared Properties, an Acknowledged event, and AcknowledgeAsync for message-processing pipelines.

SavvyioOptions collects explicit handler and dispatcher registrations and toggles discovery features such as EnableHandlerDiscovery, EnableDispatcherDiscovery, and EnableHandlerServicesDescriptor. The companion SavvyioOptionsExtensions can scan supplied assemblies for IHandler and IDispatcher implementations and populate the option lists automatically.

Basic usage

using Codebelt.Extensions.Xunit;
using Savvyio;
using Savvyio.Handlers;
using Xunit;

namespace MyProject.Tests;

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

    [Fact]
    public void RequestReplyActivator_QueryRequest_PreservesMetadata()
    {
        var request = new LookupInvoice(42)
            .SetCorrelationId("corr-42")
            .SetRequestId("req-42");

        var activator = HandlerFactory.CreateRequestReply<IRequest>(registry =>
            registry.Register<LookupInvoice, InvoiceSummary>(invoice =>
                new InvoiceSummary(invoice.InvoiceNumber, invoice.GetCorrelationId(), invoice.GetRequestId())));

        var handled = activator.TryInvoke<InvoiceSummary>(request, out var summary);

        Assert.True(handled);
        Assert.NotNull(summary);
        TestOutput.WriteLine($"Handled invoice {summary.InvoiceNumber} with correlation {summary.CorrelationId}.");
        Assert.Contains(nameof(LookupInvoice), request.GetMemberType());
        Assert.Equal("corr-42", summary.CorrelationId);
    }
}

public sealed record LookupInvoice(int InvoiceNumber) : Request;

public sealed record InvoiceSummary(int InvoiceNumber, string CorrelationId, string RequestId);

Use this pattern when you want a request type to carry Savvy I/O metadata and still be handled through a lightweight request-reply activator instead of a full dispatcher pipeline. It matters because Savvyio.Core keeps request identity, metadata, and handler selection in the same abstraction boundary that the higher-level Savvy I/O packages build on.

Installation

dotnet add package Savvyio.Core

Usage guidance

Adopt Savvyio.Core when you need the shared contracts behind request handling, metadata propagation, repository boundaries, aggregate roots, or transport-neutral messaging and you want those pieces without pulling in a more opinionated package. If you only need a specific CQRS or messaging slice, such as command, query, domain-event, or integration-event concrete types and dispatchers, one of the sibling Savvy I/O packages is the narrower choice.

Family packages