|
| 1 | +# Design Decision Log |
| 2 | + |
| 3 | +This document records key design decisions for the Restate .NET SDK, including context, |
| 4 | +alternatives considered, and rationale. |
| 5 | + |
| 6 | +## 1. Abstract Classes over Interfaces for Handler Parameters |
| 7 | + |
| 8 | +**Decision:** Handlers accept abstract class parameters (`Context`, `ObjectContext`, etc.) rather than interfaces. |
| 9 | + |
| 10 | +**Context:** The Java and Go SDKs use interfaces for their context types. We initially considered the same approach for .NET. |
| 11 | + |
| 12 | +**Rationale:** |
| 13 | +- Abstract classes allow adding new methods without breaking existing implementations (interfaces require default interface methods, which have limitations in .NET) |
| 14 | +- The `HttpContext` pattern from ASP.NET Core uses abstract classes for the same reason |
| 15 | +- Mock contexts can subclass the abstract types directly |
| 16 | +- Interfaces are still extracted (`IContext`, `IObjectContext`, etc.) for utility methods, type constraints, and documentation |
| 17 | + |
| 18 | +**Alternatives:** |
| 19 | +- Pure interfaces: rejected because adding methods would break user implementations |
| 20 | +- Default interface methods: considered, but limited tooling support and cannot hold state |
| 21 | + |
| 22 | +## 2. Interface Hierarchy (Additive, Non-Breaking) |
| 23 | + |
| 24 | +**Decision:** Extract interfaces mirroring the abstract class hierarchy: `IContext` > `ISharedObjectContext` > `IObjectContext` / `ISharedWorkflowContext` > `IWorkflowContext`. |
| 25 | + |
| 26 | +**Context:** Interfaces are valuable for utility methods (`void DoWork(IContext ctx)`), generic constraints, and API documentation, even though handlers use abstract class parameters. |
| 27 | + |
| 28 | +**Rationale:** |
| 29 | +- Enables Moq/NSubstitute mocking for helper methods and utilities |
| 30 | +- Provides clean API documentation surface |
| 31 | +- `IWorkflowContext` extends both `IObjectContext` and `ISharedWorkflowContext` via multiple interface inheritance |
| 32 | +- Interfaces expose only durable execution primitives; implementation details (DurableRandom, DurableConsole, typed client methods) stay on the abstract class |
| 33 | + |
| 34 | +## 3. BaseContext Visibility: `internal` Instead of `protected` |
| 35 | + |
| 36 | +**Decision:** Changed `SharedObjectContext.BaseContext` from `protected Context?` to `internal Context`. |
| 37 | + |
| 38 | +**Context:** The keyed context classes (ObjectContext, WorkflowContext, etc.) delegate all base operations to an inner `Context` instance. This property was originally `protected`, leaking implementation detail to external subclassers. |
| 39 | + |
| 40 | +**Rationale:** |
| 41 | +- `internal` hides the delegation pattern from the public API |
| 42 | +- Mock contexts in `Restate.Sdk.Testing` still access it via `InternalsVisibleTo` |
| 43 | +- Eliminates the nullable reference pattern (`BaseContext!`) by using `= null!` initializer |
| 44 | +- Simpler than a `ContextOperations` helper class, which wouldn't reduce override declarations |
| 45 | + |
| 46 | +## 4. Composition for Mock Context Deduplication |
| 47 | + |
| 48 | +**Decision:** Use `MockContextHelper` (composition) rather than a shared base class for mock context deduplication. |
| 49 | + |
| 50 | +**Context:** Four keyed mock contexts (MockObjectContext, MockSharedObjectContext, MockWorkflowContext, MockSharedWorkflowContext) had ~320 lines of duplicated delegation code. |
| 51 | + |
| 52 | +**Rationale:** |
| 53 | +- Mock contexts already inherit from abstract context classes (e.g., `MockObjectContext : ObjectContext`), so C# single inheritance prevents a shared mock base class |
| 54 | +- `MockContextHelper` is an internal composition class that holds the inner `MockContext` and exposes shared setup methods |
| 55 | +- Each keyed mock delegates to `_helper` for calls/sends/sleeps and to specialized stores for state/promises |
| 56 | +- Reduced ~320 lines of duplication while maintaining the same public API |
| 57 | + |
| 58 | +## 5. HandlerAttributeBase Extraction |
| 59 | + |
| 60 | +**Decision:** Extract common properties from `[Handler]` and `[SharedHandler]` into abstract `HandlerAttributeBase`. |
| 61 | + |
| 62 | +**Context:** Both handler attributes had 6 identical properties (Name, InactivityTimeout, AbortTimeout, IdempotencyRetention, JournalRetention, IngressPrivate). |
| 63 | + |
| 64 | +**Rationale:** |
| 65 | +- Eliminates property duplication — new handler options only need to be added once |
| 66 | +- Source generator is unaffected: it matches concrete type names (`HandlerAttribute`, `SharedHandlerAttribute`) and reads properties via `NamedArguments`, which works with inherited properties |
| 67 | +- Named `HandlerAttributeBase` (not `HandlerAttributeAttribute`) because it's a base for attributes, not an attribute itself |
| 68 | + |
| 69 | +## 6. RunOptions Removal |
| 70 | + |
| 71 | +**Decision:** Delete the `RunOptions` struct and remove the parameter from `Run()` overloads. |
| 72 | + |
| 73 | +**Context:** `RunOptions` was an empty `readonly record struct` with zero properties. Investigation confirmed that `DefaultContext.Run()` completely ignores the options parameter — `InvocationStateMachine.WriteRunCommand()` accepts only a name string. |
| 74 | + |
| 75 | +**Rationale:** |
| 76 | +- Dead code in a pre-1.0 alpha — removing is better than keeping a misleading empty type |
| 77 | +- If retry/retention options are needed later, they can be re-added when protocol support is wired through |
| 78 | +- Clean API is more important than speculative future compatibility |
| 79 | + |
| 80 | +## 7. Source Generator Hardening |
| 81 | + |
| 82 | +**Decision:** Add `#pragma warning disable` to generated files, null checks in deserializers, and compile-time TimeSpan validation (RESTATE009). |
| 83 | + |
| 84 | +**Rationale:** |
| 85 | +- `#pragma warning disable` prevents user analyzer configurations from producing spurious warnings on generated code |
| 86 | +- Null checks in deserializer lambdas provide clear error messages instead of NullReferenceExceptions |
| 87 | +- RESTATE009 catches invalid TimeSpan strings (e.g., `[Handler(InactivityTimeout = "invalid")]`) at compile time rather than runtime |
| 88 | + |
| 89 | +## 8. Deterministic Time in Mock Contexts |
| 90 | + |
| 91 | +**Decision:** `MockContext.Now()` returns a configurable `CurrentTime` property (default: 2024-01-01T00:00:00Z) instead of `DateTimeOffset.UtcNow`. |
| 92 | + |
| 93 | +**Rationale:** |
| 94 | +- Tests that depend on time should be deterministic |
| 95 | +- Users can set `ctx.CurrentTime = ...` to simulate specific timestamps |
| 96 | +- Default is a fixed date to make test output predictable |
| 97 | + |
| 98 | +## 9. .NET 10.0 Target Framework |
| 99 | + |
| 100 | +**Decision:** Target `net10.0` exclusively (no multi-targeting). |
| 101 | + |
| 102 | +**Context:** The SDK uses C# 14 features and .NET 10 APIs. Multi-targeting older frameworks would require conditional compilation and feature polyfills. |
| 103 | + |
| 104 | +**Rationale:** |
| 105 | +- Restate is a modern infrastructure platform — users are expected to use current .NET versions |
| 106 | +- Single target simplifies the build, eliminates `#if` directives, and allows using the latest APIs |
| 107 | +- .NET 10 is the current LTS release |
| 108 | + |
| 109 | +## 10. Typed Client Registration for Testing |
| 110 | + |
| 111 | +**Decision:** Add `RegisterClient<TClient>()` to mock contexts instead of auto-generating mock clients. |
| 112 | + |
| 113 | +**Rationale:** |
| 114 | +- Source-generated typed clients depend on internal context wiring that doesn't exist in mocks |
| 115 | +- `RegisterClient<TClient>()` lets users provide hand-crafted or Moq-based client instances |
| 116 | +- Without registration, client methods throw `NotSupportedException` with a helpful message |
| 117 | +- Simple and explicit — no magic or auto-generation needed |
| 118 | + |
| 119 | +## Comparison with Official SDKs |
| 120 | + |
| 121 | +| Feature | Java | TypeScript | Go | Rust | Python | **.NET (this)** | |
| 122 | +|---------|------|------------|-----|------|--------|-----------------| |
| 123 | +| Context types | Interfaces | Classes | Interfaces | Traits | Classes | **Abstract classes + interfaces** | |
| 124 | +| Code generation | Annotation processor | None | codegen CLI | Proc macros | None | **Roslyn source generator** | |
| 125 | +| Testing | TestRestateRuntime | Mock utilities | Minimal | Minimal | Minimal | **Mock context classes** | |
| 126 | +| Handler registration | Annotation scan | Manual | Manual | Attribute macros | Decorators | **Source generator + attributes** | |
| 127 | +| State typing | StateKey | String keys | String keys | String keys | String keys | **StateKey\<T\>** | |
| 128 | +| Protocol version | v5-v6 | v5-v6 | v5-v6 | v5-v6 | v5-v6 | **v5-v6** | |
0 commit comments