Portia is a cohesive application framework for .NET 10. It carries an operation from its entry point through authorization and domain decisions, then runs the durable work that follows.
ASP.NET Core already does HTTP well. Portia is the application model behind HTTP—and behind RPC, queues, notices, schedules, and local calls. A request keeps the same handler, actor, authorization, causation, result, and telemetry semantics regardless of how it arrives.
HTTP / RPC / queue / schedule / local
│
▼
request → authorization → behaviors → guards → handler → aggregate → Fitz event log
│
┌───────────────┴───────────────┐
▼ ▼
projector + checkpoint reactor + effect
│
▼
Cassie / PostgreSQL / Snowflake / ...
The unit of value is not command → handler → route. It is the complete path from intent to durable consequence.
- One application operation, many entry points. A handler is selected once and can be reached locally or through an explicitly allowed transport without transport logic entering the handler.
- One execution context. Actor identity, authorization, correlation, causation, expected failures, tracing, and metrics follow the request across process and delivery boundaries.
- Event-sourced decisions. Aggregates are ordinary application objects. Portia hydrates and saves their events with optimistic concurrency and stable event identities.
- Durable consequences. Projectors atomically commit read-model changes with their checkpoints. Reactors run external effects at least once with preserved event causation. Failed work never silently advances progress.
- One composition, independent deployments. An API and its workers share application setup but can be deployed and scaled separately. Workers support global, per-tenant, and fleet-coordinated ownership.
- Compile-time wiring. Registration, transport descriptors, HTTP binding, and JSON roots are source-generated and checked by Portia analyzers. The supported runtime path uses no assembly scanning or reflection and is exercised as a packed NativeAOT application in CI.
- Reusable request preflight. Scoped asynchronous guards run at the unary handler boundary, can target one request
or an application-owned request family, and short-circuit with the ordinary
Resultcontract.
A request declares its stable identity and the transports the application permits:
[RequestRoute("banking", "accounts", "*", "deposit")]
[Discriminator("accounts.deposit")]
public sealed record Deposit(Uuid AccountId, int Amount)
: IRequest, ICallable, IQueuable;Its handler contains the application decision, not HTTP or queue plumbing:
public sealed class DepositHandler(IAggregateExecutor aggregates)
: IRequestHandler<Deposit>
{
public ValueTask<Result> HandleAsync(
IRequestContext<Deposit> context,
CancellationToken ct) =>
aggregates.ExecuteAsync(
new Account(context.Request.AccountId),
account => AggregateOutcome.Commit(account.Deposit(context.Request.Amount)),
context,
ct);
}The application registers the behavior and the durable work caused by its events:
services.AddPortia()
.AddRequestHandler<DepositHandler>()
.AddProjector<AccountBalanceProjector>("AccountBalanceProjector", WorkloadScope.PerTenant)
.AddReactor<DepositReceiptReactor>("DepositReceiptReactor", WorkloadScope.PerTenant)
.AddFitz(configuration.GetSection("Fitz"));Per-tenant projectors and reactors declare their source with
EventStreamPattern.ForTenant(area, resource); Portia binds the actual tenant realm before reading. Global workloads
use an exact EventStreamPattern.ForPattern(realm, area, resource).
An API host explicitly maps HTTP endpoints. A worker host calls the same application setup and adds .AddWorkers(). The
handler and domain model do not change when the operation is sent over RPC, placed on a queue, or invoked in-process.
The complete consumer fixture executes one business handler through direct dispatch, HTTP, RPC, and a real Fitz queue, then proves that the resulting events retain actor and causal metadata and drive projectors and reactors.
Fitz is Portia's first-party event log, transport, scheduler, and coordination layer. Cassie is the first-party read-model engine for SQL, graph, time-series, and vector workloads.
Neither is hidden behind a generic query language. A projector receives an ordinary application repository built on the
backend's native client and schema. That repository implements
IProjectionStore and IProjectionBatch only to give Portia the lifecycle and atomic read-model-plus-checkpoint
boundary it needs.
PostgreSQL, Snowflake, or another store can therefore be added in userland without changing Portia, writing a custom runner, or translating queries into a framework DSL. If a target cannot atomically commit its changes and checkpoint, it is modeled honestly as an at-least-once reactor effect instead.
- Getting started: packages, contracts, registration, HTTP, and transports
- Shared application setup: API and worker deployments from one composition
- Projectors and reactors: native repositories and durable processing
- Request context: actor, correlation, and causation
- Request guards: authorization, reusable asynchronous preflight, and authoritative invariants
- Platform vision: the fixed Fitz–Portia–Cassie boundary
- Scope and design decisions: guarantees and deliberate limits
- Performance and scaling: measured hot paths and scaling model
- NativeAOT: trimming and source-generated JSON setup
- Model Context Protocol: generated tools over stdio and Streamable HTTP
- Migrating to Portia 0.4: JSON ownership, tenant templates, and MCP tests
dotnet format Portia.slnx --verify-no-changes
dotnet build Portia.slnx --configuration Release
dotnet test Portia.slnx --configuration Release --no-build --filter "Category!=BrokerIntegration"
docker compose up --detach --wait fitz
dotnet test Portia.slnx --configuration Release --no-build --filter "Category=BrokerIntegration"
docker compose down --volumesBroker integration uses ws://127.0.0.1:4090/ws by default. Override it with
FITZ_TEST_ENDPOINT. CI runs the same format, build, broker-free test, packed-consumer, and broker integration
sequence.
Portia is licensed under the Apache License, Version 2.0.