Codebelt

Savvyio.Queries

In-process query-side CQRS primitives for defining queries, registering handlers, and dispatching request-reply flows.

.NET 10.0 / .NET 9.0 MIT v5.0.8 10,913 downloads

Overview

Savvyio.Queries supplies the query-side CQRS primitives for Savvy I/O. It extends Savvyio.Core with a concrete Query<TResult> base record, a request-reply QueryDispatcher, and a QueryHandler base class for registering delegates that answer specific query types.

Use this package when queries stay in-process and you want an explicit dispatcher and handler boundary instead of calling repositories or services directly from application code. Persistence, dependency injection integration, and transport concerns are intentionally handled outside this package.

Key APIs

Query<TResult> is the abstract base record for concrete queries. It derives from Request, so the runtime member type is already stored in metadata, and its constructor merges any optional metadata into the new query instance.

QueryDispatcher.Query and QueryDispatcher.QueryAsync execute an IQuery<TResult> through the request-reply pipeline exposed by IQueryHandler. Both methods validate that the request is non-null, and the underlying dispatcher throws OrphanedHandlerException when no resolved handler can answer the query.

QueryHandler is the abstract handler base for query processing. Its constructor creates the Delegates activator through HandlerFactory.CreateRequestReply<IQuery>, and consumers implement RegisterDelegates(IRequestReplyRegistry<IQuery> handlers) to map concrete query types to synchronous or asynchronous result delegates.

SavvyioOptionsExtensions.AddQueryHandler<TImplementation> records an IQueryHandler implementation in SavvyioOptions. The generic constraint requires a concrete class that implements IQueryHandler, which keeps registration explicit.

SavvyioOptionsExtensions.AddQueryDispatcher records the default IQueryDispatcher to QueryDispatcher mapping in SavvyioOptions. It is the package's built-in composition hook for enabling query dispatch in a wider Savvy I/O setup.

Basic usage

using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Codebelt.Extensions.Xunit;
using Cuemon;
using Savvyio.Dispatchers;
using Savvyio.Handlers;
using Savvyio.Queries;
using Xunit;

namespace MyProject.Tests;

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

    [Fact]
    public void QueryDispatcher_ConfiguredHandler_ReturnsResponse()
    {
        var dispatcher = new QueryDispatcher(new ServiceLocator(serviceType =>
            serviceType == typeof(IQueryHandler)
                ? (IEnumerable<object>)new IQueryHandler[] { new InvoiceSummaryHandler() }
                : Array.Empty<object>()));

        var summary = dispatcher.Query(new GetInvoiceSummary(42));

        TestOutput.WriteLine($"Invoice {summary.InvoiceNumber} resolved as {summary.DocumentNumber}.");
        Assert.Equal(42, summary.InvoiceNumber);
        Assert.Equal("INV-0042", summary.DocumentNumber);
    }
}

public sealed record GetInvoiceSummary(int InvoiceNumber) : Query<InvoiceSummary>;

public sealed record InvoiceSummary(int InvoiceNumber, string DocumentNumber);

public sealed class InvoiceSummaryHandler : IQueryHandler
{
    public IRequestReplyActivator<IQuery> Delegates { get; } = new InvoiceSummaryActivator();
}

public sealed class InvoiceSummaryActivator : IRequestReplyActivator<IQuery>
{
    public bool TryInvoke<TResponse>(IQuery request, out TResponse result)
    {
        if (request is GetInvoiceSummary query && typeof(TResponse) == typeof(InvoiceSummary))
        {
            result = (TResponse)(object)new InvoiceSummary(query.InvoiceNumber, $"INV-{query.InvoiceNumber:D4}");
            return true;
        }

        result = default!;
        return false;
    }

    public Task<ConditionalValue<TResponse>> TryInvokeAsync<TResponse>(IQuery request, CancellationToken ct = default)
        => throw new NotSupportedException();
}

Use this pattern when you want explicit Query<TResult> models routed through QueryDispatcher in an in-process request-reply flow. It matters because the dispatcher resolves the matching IQueryHandler and the underlying request-reply pipeline throws OrphanedHandlerException when no handler can answer a query.

Installation

dotnet add package Savvyio.Queries

Usage guidance

Choose Savvyio.Queries when your application benefits from explicit query types, delegate-based query handlers, and an in-process request-reply dispatcher that can be composed into a larger Savvy I/O setup. If a query is just a direct repository or data-store call with no need for a dispatcher boundary, plain application code or the lower-level abstractions in Savvyio.Core are the smaller choice.

Family packages