Overview
Savvy I/O is a .NET library family for keeping commands, queries, domain changes, and distributed messages in the same model instead of treating them as unrelated layers. It starts with shared request and metadata contracts, then adds domain modeling, event sourcing, persistence adapters, serializers, brokers, and DI wrappers so you can compose only the boundaries your application needs. Savvyio.App exists for teams that want the combined stack in one reference, but the repository is organized so the smaller packages still stand on their own.
Concepts
The repository makes the most sense as a layered set of contracts and adapters. The inner packages define request, metadata, and domain rules, and the outer packages adapt those rules to storage, serialization, transport, and application composition.
Request boundaries and shared metadata
Savvyio.Core is the common contract layer for the entire repository. It defines the request base type, metadata propagation rules, handler activation, fire and forget versus request reply dispatch, repository and data store abstractions, aggregate contracts, and the transport neutral IMessage<T> shape that outer packages build on.
That shared metadata surface is one of the repository's important design choices. Correlation identifiers, request identifiers, event identifiers, timestamps, member types, and arbitrary metadata can move with a request or event before it ever reaches a serializer, repository, or broker specific adapter.
In-process command and query flows
Savvyio.Commands and Savvyio.Queries turn the core request model into explicit CQRS boundaries. Commands are fire and forget, queries are request reply, and both use delegate based handler registration so missing handlers fail fast instead of disappearing behind a no-op pipeline.
Savvyio.Extensions.Dispatchers matters when application code wants one mediator entry point instead of four separate dispatcher dependencies. It does not replace the underlying command or query concepts. It composes them so the same application can commit commands, execute queries, and later expand into domain or integration event dispatch without inventing a second calling model.
Domain aggregates and event streams
Savvyio.Domain brings DDD concepts into the same request and metadata system. Entities, aggregate roots, value objects, and domain events all follow shared conventions, and aggregate roots can collect pending domain events for later dispatch.
Savvyio.Domain.EventSourcing changes the aggregate model in a specific way rather than adding generic persistence. Traced aggregate roots replay versioned traced domain events through registered delegates, which makes event replay and new state transitions use the same code path. That separation is important because plain aggregates and event sourced aggregates are not interchangeable modeling choices.
Message envelopes and integration events
Savvyio.Messaging is the broker neutral envelope layer. It adds message identity, source, type, acknowledgement hooks, shared properties, and optional signing around any IRequest payload, which is why the transport packages can stay focused on delivery mechanics instead of redefining payload contracts.
On top of that, Savvyio.Commands.Messaging wraps commands for point to point delivery, while Savvyio.EventDriven and Savvyio.EventDriven.Messaging define the integration event side of the model. Integration events can stay in process, move into message envelopes, project into CloudEvents, and carry optional signatures without changing the event payload type itself.
Persistence for projections, entities, and replay
The persistence packages separate projection style access from aggregate persistence. Savvyio.Extensions.Dapper and Savvyio.Extensions.DapperExtensions are centered on data stores, connections, SQL or auto mapped CRUD, and query options for DTO or read model oriented access.
The EF Core side is more repository and unit of work oriented. Savvyio.Extensions.EFCore provides EF Core backed data sources, repositories, and data stores, Savvyio.Extensions.EFCore.Domain coordinates aggregate persistence with pending domain event dispatch at save time, and Savvyio.Extensions.EFCore.Domain.EventSourcing persists traced aggregate streams as serialized event rows and rehydrates aggregates by replay.
Serialization is an explicit adapter
The repository does not bake one JSON library into the model. Savvyio.Extensions.Text.Json and Savvyio.Extensions.Newtonsoft.Json both serialize Savvy I/O requests, metadata dictionaries, envelopes, CloudEvents, and domain value types through IMarshaller, which keeps transport and persistence packages independent from a single serializer choice.
That matters beyond simple formatting preferences. Event stream persistence, signed messages, CloudEvent projection, and broker transports all depend on a marshaller contract, so serializer choice is an outer boundary decision. The DI wrappers Savvyio.Extensions.DependencyInjection.Text.Json and Savvyio.Extensions.DependencyInjection.Newtonsoft.Json exist to register one shared marshaller configuration instead of scattering serializer setup throughout the application.
Transport adapters keep the same envelope model
The transport packages adapt the same command and integration event envelopes to different delivery systems. Savvyio.Extensions.NATS targets NATS work queues and publish subscribe, Savvyio.Extensions.QueueStorage targets Azure Queue Storage and Azure Event Grid, Savvyio.Extensions.RabbitMQ targets RabbitMQ work queues and fanout exchanges, and Savvyio.Extensions.SimpleQueueService targets AWS SQS and SNS.
The non-obvious point is that transport choice changes queue, exchange, topic, client, and acknowledgement behavior more than it changes the application payload model. Because those packages consume the same message and CloudEvent abstractions from Savvyio.Messaging, Savvyio.Commands.Messaging, and Savvyio.EventDriven.Messaging, you do not need a different command or event type for each broker.
Marker-based dependency injection composition
Savvyio.Extensions.DependencyInjection is the common DI layer for the repository. It adds marker aware contracts for data sources, repositories, stores, units of work, marshallers, and message channels, plus assembly scanning and AddSavvyIO helpers for handlers and dispatchers.
The marker pattern is what ties the rest of the DI packages together. Savvyio.Extensions.DependencyInjection.Domain, Savvyio.Extensions.DependencyInjection.Dapper, Savvyio.Extensions.DependencyInjection.DapperExtensions, Savvyio.Extensions.DependencyInjection.EFCore, Savvyio.Extensions.DependencyInjection.EFCore.Domain, Savvyio.Extensions.DependencyInjection.EFCore.Domain.EventSourcing, Savvyio.Extensions.DependencyInjection.NATS, Savvyio.Extensions.DependencyInjection.QueueStorage, Savvyio.Extensions.DependencyInjection.RabbitMQ, and Savvyio.Extensions.DependencyInjection.SimpleQueueService all use markers so one application can host multiple implementations of the same abstraction without falling back to registration order or separate service provider hacks.
Usage guidance
Start from the smallest package that matches the boundary you actually need. Use Savvyio.Core for shared request and metadata rules, add Savvyio.Commands or Savvyio.Queries for in-process CQRS, move to Savvyio.Domain only when aggregate and value object semantics matter, and adopt Savvyio.Domain.EventSourcing only when replayed event streams are part of the model rather than an implementation detail. Choose one persistence path and one serializer first, then add one transport package if the payload must leave the process. Reach for Savvyio.App when you already know that broad package closure is desirable, not as the default starting point.
A common mistake is to let the outer adapter decide the model. Broker packages, serializer packages, and DI wrappers are intentionally thin compared with the core request, domain, and messaging layers, so choose the payload shape and dispatch style before choosing NATS, Azure, RabbitMQ, AWS, Dapper, or EF Core. Use the marker based DI packages only when you truly need multiple implementations of the same abstraction in one container, because they solve an important composition problem but also make the dependency graph more explicit and more complex by design.
Family packages
- 🏭Savvyio.App
- 📦Savvyio.Commands
- 📦Savvyio.Commands.Messaging
- 📦Savvyio.Core
- 📦Savvyio.Domain
- 📦Savvyio.Domain.EventSourcing
- 📦Savvyio.EventDriven
- 📦Savvyio.EventDriven.Messaging
- 📦Savvyio.Extensions.Dapper
- 📦Savvyio.Extensions.DapperExtensions
- 📦Savvyio.Extensions.DependencyInjection
- 📦Savvyio.Extensions.DependencyInjection.Dapper
- 📦Savvyio.Extensions.DependencyInjection.DapperExtensions
- 📦Savvyio.Extensions.DependencyInjection.Domain
- 📦Savvyio.Extensions.DependencyInjection.EFCore
- 📦Savvyio.Extensions.DependencyInjection.EFCore.Domain
- 📦Savvyio.Extensions.DependencyInjection.EFCore.Domain.EventSourcing
- 📦Savvyio.Extensions.DependencyInjection.NATS
- 📝Savvyio.Extensions.DependencyInjection.Newtonsoft.Json
- 📦Savvyio.Extensions.DependencyInjection.QueueStorage
- 📦Savvyio.Extensions.DependencyInjection.RabbitMQ
- 📦Savvyio.Extensions.DependencyInjection.SimpleQueueService
- 📝Savvyio.Extensions.DependencyInjection.Text.Json
- 📦Savvyio.Extensions.Dispatchers
- 📦Savvyio.Extensions.EFCore
- 📦Savvyio.Extensions.EFCore.Domain
- 📦Savvyio.Extensions.EFCore.Domain.EventSourcing
- 📦Savvyio.Extensions.NATS
- 📝Savvyio.Extensions.Newtonsoft.Json
- 📦Savvyio.Extensions.QueueStorage
- 📦Savvyio.Extensions.RabbitMQ
- 📦Savvyio.Extensions.SimpleQueueService
- 📝Savvyio.Extensions.Text.Json
- 📦Savvyio.Messaging
- 📦Savvyio.Queries