Share via

Microsoft Orleans grain: Should polymorphism be achieved at the grain or grain stat level?

Michał Śmiechowicz 0 Reputation points
2026-03-17T10:54:11.9133333+00:00

I'm wondering whether to achieve the advantages of polymorphism I should use IGrainC : IGrainB : IGrainA or persistence, for example: classC : classB : classA. and in a common grain, e.g. IGrainCommon IPersistentState

Developer technologies | .NET | Other

2 answers

Sort by: Most helpful
  1. Jack Dang (WICLOUD CORPORATION) 15,870 Reputation points Microsoft External Staff Moderator
    2026-03-18T04:05:34.12+00:00

    Hi @Michał Śmiechowicz ,

    Thanks for reaching out.

    Regarding your concern, it really comes down to where the differences in your system actually live - in behavior or just in data.

    If your different types (A, B, C) have different logic or behavior, then it’s better to model that at the grain level using interfaces like IGrainC : IGrainB : IGrainA. In Microsoft Orleans, grains are meant to encapsulate behavior, so this approach keeps things clean and aligned with how Orleans is designed. Each grain type can implement its own logic without needing a lot of conditional checks.

    You can find more details here: https://learn.microsoft.com/en-us/dotnet/orleans/grains/

    On the other hand, if the differences are mostly about data shape or stored state, and the behavior is largely the same, then using a shared grain with polymorphic state (e.g. classC : classB : classA with IPersistentState) can work well. This keeps the grain surface simpler, but you may end up adding type checks or branching logic inside the grain if behavior starts to diverge.

    More on grain persistence and state: https://learn.microsoft.com/en-us/dotnet/orleans/grains/grain-persistence/

    A good rule of thumb is:

    • Use grain/interface inheritance when behavior differs
    • Use state inheritance when only the data differs

    If you’re unsure, it’s usually safer to lean toward grain-level polymorphism, because it avoids mixing multiple behaviors into a single grain and keeps things easier to maintain as your system grows.

    You might also find the general design guidance helpful: https://learn.microsoft.com/en-us/dotnet/orleans/resources/best-practices/

    Hope this helps! If my answer was helpful, I would greatly appreciate it if you could follow the instructions here so others with the same problem can benefit as well.


  2. Q&A Assist
    2026-03-17T10:54:31.5566667+00:00
    AI generated content. This question contains an answer created with AI from Q&A Assist. Learn more

    In Orleans, polymorphism is typically modeled at the grain interface and state type level, but with some clear constraints.

    Key points from the Orleans model in the context:

    1. Grain identity and behavior are separate from storage
      Grains are defined by their identity and behavior (interfaces such as IGrainWithGuidKey, IGrainWithStringKey, etc.) and can have volatile or persistent state:
      • Grain identity is defined via interfaces like IGrainWithGuidKey, IGrainWithIntegerKey, IGrainWithStringKey, etc. b55049ee-d13e-c519-6cc9-0e1e129f4604-004
      • State can be volatile or persisted using grain persistence APIs.
    2. Preferred persistence model is IPersistentState<TState>
      Grain<TGrainState> is considered legacy for adding storage to a grain. The recommended approach is IPersistentState<TState>:
    3. One storage provider per grain/state declaration
      A grain (or a PersistentStateAttribute-decorated state) is associated with a single storage provider instance:
    4. Serialization supports polymorphic state types
      Orleans’ serialization framework supports dynamic types and arbitrary polymorphism:
      • If an interface expects a base type (e.g., IDictionary) and a derived type is passed (e.g., SortedDictionary), the actual derived type is preserved across calls. cea5c9e0-8887-9e61-c735-27ed81d6cf37-020
      • The same serialization system is used for grain persistent state objects.

    Given these points, for the specific design choice:

    • Using interface inheritance for grains (IGrainC : IGrainB : IGrainA) is a good way to express behavioral polymorphism (shared methods, contracts, and identity semantics). This is how grain behavior is typically modeled.
    • For state polymorphism, using a common base state type (e.g., BaseState) with derived types (StateA, StateB, StateC) and a single IPersistentState<BaseState> is compatible with Orleans’ serialization model, which supports polymorphism.
    • The concern about multiple [PersistentState("a"|"b"|"c", "mongodb")] names is valid: each named state is bound to a specific storage configuration. Best practice is to keep the number of distinct storage names minimal and model variation in the state schema rather than multiplying storage names unless there is a clear operational reason.

    In practice, a common pattern is:

    • Define a base grain interface for shared behavior and derive more specific interfaces from it.
    • Use one state field per grain (via IPersistentState<TState> or Grain<TState> if legacy) with a base state type and derived state types if polymorphism in data is needed.
    • Keep storage configuration simple (few provider names, few state names) and let the state type hierarchy and Orleans serialization handle polymorphism.

    This aligns with the guidance that:

    So, modeling polymorphism primarily in the state hierarchy (with a single IPersistentState<BaseState> per grain) combined with interface inheritance for behavior is consistent with Orleans best practices and avoids proliferation of separate persistent state names like "a", "b", and "c" unless there is a strong configuration or operational need.


    References:

    0 comments No comments

Your answer

Answers can be marked as 'Accepted' by the question author and 'Recommended' by moderators, which helps users know the answer solved the author's problem.