Comparison
"The best framework is the one you don't have to fight. The second best is the one that fights for you at compile time."
Amplification Ratio
A typical DistributedTask declaration — the attributes, the request class, the step overrides — weighs in around 55 lines of developer-written code. From those 55 lines, the source generator produces 700+ lines of infrastructure:
- Saga orchestrator with state machine
- Controller with submit, cancel, status endpoints
- Queue consumer with deserialization and dispatch
- Per-step retry wrappers with Polly
- Compensation handlers with rollback ordering
- OpenTelemetry spans per step
- Redis distributed locking
- Idempotency deduplication
- S3 upload/download plumbing
- Five listening transports (Polling, SSE, WebSocket, SignalR, Webhook)
- Health checks and metrics
- Strongly-typed request/response DTOs
That is a ~13x amplification ratio. The developer writes the "what." The generator writes the "how."
DistributedTask.Dsl vs MassTransit Sagas
MassTransit is a mature messaging abstraction for .NET. Its saga state machines are powerful — but they are imperative. You write a class that inherits MassTransitStateMachine<T>, define states, events, transitions, and activities in code. You wire message consumers manually. You handle compensation yourself.
DistributedTask.Dsl is declarative. You annotate a class with [DistributedTask], mark steps with [SagaStep], and the generator produces the state machine. Compensation is declared with [CompensationFor]. Retry is declared with [RetryPolicy].
| Aspect | MassTransit | DistributedTask.Dsl |
|---|---|---|
| Paradigm | Imperative state machine class | Declarative attributes |
| Validation | Runtime (exceptions on misconfiguration) | Compile-time (Roslyn analyzers) |
| Compensation | Manual (you write it) | Generated orchestration (you write the handler) |
| Transport | Abstracted (RabbitMQ, Azure SB, etc.) | RabbitMQ (explicit, not abstracted) |
| Learning curve | Medium-high | Low (attributes + Generation Gap overrides) |
MassTransit is a library — it runs your code at runtime. DistributedTask.Dsl is a code generator — it writes your code at compile time.
DistributedTask.Dsl vs Temporal / Durable Functions
Temporal (and Azure Durable Functions) use replay-based recovery: when a workflow fails, the engine replays the workflow function from the beginning, skipping already-completed steps using a journal. This is elegant but requires a dedicated server (Temporal Server or Azure infrastructure) and imposes constraints on workflow code (must be deterministic, no side effects in the replay path).
DistributedTask.Dsl uses saga-pattern compensation: when a step fails after retries, the orchestrator runs compensation handlers in reverse order. No replay. No dedicated server. The infrastructure is self-hosted: MinIO for S3-compatible storage, RabbitMQ for messaging, Redis for locking and pub/sub.
| Aspect | Temporal / Durable Functions | DistributedTask.Dsl |
|---|---|---|
| Recovery model | Replay-based journal | Saga compensation |
| Infrastructure | Temporal Server or Azure | Self-hosted (MinIO + RabbitMQ + Redis) |
| Language support | Multi-language SDKs | C# only |
| Determinism constraint | Strict (workflow must be deterministic) | None (steps are independent units) |
| Cloud dependency | Yes (Temporal Cloud or Azure) | None (runs on-prem, in Docker, anywhere) |
If your team already runs Temporal in production and needs multi-language workflows, use Temporal. If you are a .NET shop that wants compile-time safety and self-hosted infrastructure, DistributedTask.Dsl is the lighter path.
DistributedTask.Dsl vs Hangfire
Hangfire is a job scheduler. It excels at fire-and-forget background work: send an email, generate a report, clean up old records. It has a nice dashboard and supports delayed and recurring jobs.
But Hangfire is not a saga orchestrator. It has no concept of multi-step workflows with typed data flowing between steps. No compensation. No fan-out/fan-in. No distributed locking. No S3-first-class integration.
Use Hangfire for simple background jobs. Use DistributedTask.Dsl when the job is a saga.
DistributedTask.Dsl vs Camunda / Zeebe
Camunda is a full Business Process Management (BPM) engine with a BPMN visual designer. Non-technical stakeholders can draw process flows. It supports long-running human tasks, complex routing, DMN decision tables, and multi-tenant process deployment.
DistributedTask.Dsl is not a BPM engine. It is a lightweight C# DSL for developers who think in code, not diagrams. It generates infrastructure for technical sagas — file processing, data pipelines, integration workflows — where every participant is a machine.
| Aspect | Camunda / Zeebe | DistributedTask.Dsl |
|---|---|---|
| Audience | Business analysts + developers | Developers only |
| Process definition | BPMN XML / visual designer | C# attributes |
| Runtime | JVM (Zeebe is Go + Java clients) | .NET |
| Human tasks | First-class | Not supported |
| Weight | Heavy (full BPM platform) | Lightweight (source generator) |
If your processes involve human approvals, regulatory compliance, and non-technical stakeholders drawing diagrams, use Camunda. If your processes are technical sagas written by developers, use DistributedTask.Dsl.
Feature Comparison Matrix
| Feature | DistributedTask.Dsl | MassTransit | Temporal | Hangfire | Camunda |
|---|---|---|---|---|---|
| Typed request/response | Yes (generated DTOs) | Manual | Yes (SDK) | No | No (XML) |
| Saga compensation | Yes (generated) | Manual | Replay-based | No | Yes (BPMN) |
| Per-step retry | Yes ([RetryPolicy]) |
Manual (Polly) | Built-in | Global only | Yes (BPMN) |
| Fan-out/fan-in | Yes ([Parallel]) |
Manual | Yes | No | Yes (BPMN) |
| Client-chosen notifications | Yes (5 transports) | No | No | Polling only | No |
| Compile-time validation | Yes (14 analyzers) | No | No | No | No |
| Generation Gap overrides | Yes | N/A | N/A | N/A | N/A |
| Distributed locking | Yes (RedLock) | No | Built-in | No | Built-in |
| Idempotency | Yes (generated) | Manual | Built-in | No | Built-in |
| OpenTelemetry | Yes (generated spans) | Yes (built-in) | Yes (built-in) | No | Partial |
| S3 first-class | Yes ([S3Upload]) |
No | No | No | No |
| InProcess dev mode | Yes | No | Local dev server | In-memory | No |
Compile-Time Diagnostics
The source generator ships 14 Roslyn analyzers that catch misconfigurations before the code compiles:
| ID | Severity | Rule | Fires When |
|---|---|---|---|
| DST001 | Error | Missing [TaskRequest] |
[DistributedTask] class has no request type |
| DST002 | Error | Missing [TaskResponse] |
[DistributedTask] class has no response type |
| DST003 | Error | Duplicate step order | Two [SagaStep] attributes share the same Order value |
| DST004 | Error | Compensation target not found | [CompensationFor] references a step name that does not exist |
| DST005 | Error | Circular step dependency | Step graph contains a cycle |
| DST006 | Warning | Missing compensation | A step that modifies external state has no [CompensationFor] handler |
| DST007 | Error | Invalid retry configuration | MaxRetries is negative or DelayMs is zero with exponential backoff |
| DST008 | Warning | Unreachable step | A step has no incoming dependency and is not the first step |
| DST009 | Error | Fan-out without fan-in | [Parallel] group has no corresponding join step |
| DST010 | Error | S3 bucket not configured | [S3Upload] or [S3Download] used but no [S3Configuration] on the task |
| DST011 | Warning | Large step count | Saga has more than 20 steps (likely a design smell) |
| DST012 | Error | InProcess mode with distributed lock | [DistributedLock] used in InProcess mode (no Redis available) |
| DST013 | Warning | Webhook without HMAC | ListeningStrategy.Webhook enabled but no [WebhookHmacSecret] configured |
| DST014 | Error | Duplicate task name | Two [DistributedTask] classes share the same name |
These diagnostics appear in the IDE as you type — red squiggles, warnings in the Error List, and CI build failures. No runtime surprises.
Use DistributedTask.Dsl when:
- You are a .NET shop and want to stay in C#
- Your workflows follow the saga pattern (multi-step, compensatable)
- You process files through S3 (uploads, transformations, downloads)
- You want compile-time safety — analyzers catch errors before runtime
- You want Generation Gap overrides — customize generated code without forking
- You need multiple notification transports (SSE, WebSocket, SignalR, Webhook, Polling)
- You prefer self-hosted infrastructure over cloud dependencies
Use MassTransit when:
- You are already invested in MassTransit's ecosystem (consumers, sagas, middleware)
- You need transport abstraction (switch between RabbitMQ, Azure Service Bus, Amazon SQS)
- Your team is comfortable with imperative state machine code
Use Temporal when:
- You need replay-based recovery with a durable execution journal
- Your team works in multiple languages (Go, Java, Python, TypeScript, .NET)
- You are cloud-native and can run Temporal Server (self-hosted or Temporal Cloud)
- You need long-running workflows (days, weeks) with sleep/timer support
Use Hangfire when:
- You need simple background jobs — send email, generate report, clean up
- You want a dashboard out of the box
- You do not need compensation, saga orchestration, or typed step data
Use Camunda when:
- Non-technical stakeholders design and modify business processes
- You need BPMN compliance for regulatory or audit reasons
- You need human task management (approvals, reviews, escalations)
- You are willing to run a JVM-based BPM platform
What's Next
Part XVI: Security — authorization handlers, HMAC-signed webhooks, and queue encryption. The final piece before the system is production-ready.