You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
@@ -243,6 +244,86 @@ public class LongRunningTaskGrain(
243
244
}
244
245
```
245
246
247
+
---
248
+
### `IDurableObject<T>`
249
+
250
+
A streamlined container for a single complex object (POCO) that allows for direct mutation of its properties. The idea behind `IDurableObject<T>` came from the desire to create the "best of both worlds": an API with the simplicity of `IDurableValue<T>`, but with the ability to mutate a complex object directly like `IPersistentState<T>`.
251
+
252
+
**Useful for:** Managing any complex state class, like a `UserProfile`, `GameSession`, or `OrderDetails`, without the ceremony of creating new instances for every change.
253
+
254
+
```csharp
255
+
// Direct mutation is the intended pattern.
256
+
257
+
state.Value.Counter++;
258
+
state.Value.LastUpdated=DateTime.UtcNow;
259
+
state.Value.Nested.Name="New Nested Name";
260
+
261
+
awaitWriteStateAsync();
262
+
263
+
```
264
+
265
+
#### Why not use `IDurableValue<T>` for complex objects?
266
+
267
+
While possible, it's cumbersome and dare I say even error-prone. You must create a new object instance for every change, which can lead to verbose code and bugs if you forget to copy a property.
268
+
269
+
```csharp
270
+
// Assume you have an IDurableValue<MyState> named 'state'.
271
+
272
+
// ------------- Incorrect approach -------------
273
+
274
+
// We intuitively mutate the object directly.
275
+
// This changes the in-memory object, but the state machine
276
+
// doesn't know a change occurred.
277
+
278
+
// The setter for 'state.Value' was never called,
279
+
// so this write does nothing.
280
+
281
+
state.Value.Counter++;
282
+
awaitWriteStateAsync();
283
+
284
+
// After a re-activation, the counter change above will be lost.
285
+
286
+
// ------------- Correct approach -------------
287
+
288
+
varnewState=newMyState
289
+
{
290
+
Counter=state.Value.Counter+1,
291
+
Name=state.Value.Name
292
+
};
293
+
294
+
// We need to assign the new instance to trigger the setter.
295
+
296
+
state.Value=newState;
297
+
awaitWriteStateAsync();
298
+
299
+
// Now the change is persisted correctly.
300
+
```
301
+
302
+
#### Why not just use `IPersistentState<T>`?
303
+
304
+
`DurableState<T>` (*the implementation of `IPersistentState<T>`*) exists primarily for **familiarity and migration** from the classic Orleans state model. It works, but it **has to** bring along some jargon and potential confusion:
305
+
306
+
-**Extra API Surface:** It exposes the full `IStorage<T>` contract, including `Etag`, `ClearStateAsync()`, etc., which are not needed when using `DurableGrain`.
307
+
308
+
-**Confusing Write Calls:** You can call `state.State.WriteStateAsync()`, which feels disconnected from the `DurableGrain`'s own `WriteStateAsync()` method.
309
+
310
+
`IDurableObject<T>` is the purpose-built, ergonomic choice for this library. It provides only the essential features (`Value`, `RecordExists`) in a cleaner package, making your grain code simpler and more predictable.
311
+
312
+
#### Why Can't `Value` be set to `null`?
313
+
314
+
This is a deliberate design choice for safety and predictability. The `get` accessor for `Value` guarantees it will never return `null` (*it creates a new instance if one doesn't exist*). Allowing the setter to accept `null` would create a confusing and inconsistent state.
315
+
316
+
```csharp
317
+
// This is NOT allowed and will throw an ArgumentNullException:
318
+
state.Value=null;
319
+
```
320
+
321
+
By disallowing `null`, `IDurableObject<T>` ensures that you can always safely access and mutate the state object without needing to perform null checks, preventing a common source of `NullReferenceException`s.
322
+
323
+
#### Why no `Etag`?
324
+
325
+
An `Etag` is a token used to detect race conditions where multiple processes might update the same record. This component doesn't need one because Orleans ensures only a single grain activation is the "writer," and all changes are appended to an immutable log using a custom binary protocol. Any process attempting to write to the log outside the state machine would corrupt the state, making external updates inherently not feasible, and even unsafe.
326
+
246
327
---
247
328
248
329
If you find it helpful, please consider giving it a ⭐ and share it!
0 commit comments