Skip to content

Entities do not support non-JSON serializable states #437

@hossam-nasr

Description

@hossam-nasr

If you try to set a durable entity state to a non-serializable object, for example a rich class, the methods of that class would not be available and your entity/orchestration would fail with an error. The simplest example of this is the ShoppingEntity/ShoppingOrchestration example in the samples/ directory:

ShoppingEntity/index.ts:

import * as df from "durable-functions";
import { CartItem, ShoppingCart, Operations } from "../Shopping/data";

module.exports = df.entity<ShoppingCart>(function (context) {
  const cart = context.df.getState(() => new ShoppingCart());

  switch (context.df.operationName) {
    case Operations.ADD_ITEM:
      const cartItem = context.df.getInput<CartItem>();
      cart.addItem(cartItem);
      break;
    case Operations.REMOVE_ITEM:
      const itemToRemove = context.df.getInput<string>();
      cart.remoteItem(itemToRemove);
      break;
    case Operations.GET_VALUE:
      const value = cart.getCartValue();
      context.df.return(value);
      break;
  }

  context.df.setState(cart);
});

ShoppingOrchestration/index.ts:

import * as df from "durable-functions";
import { items, Operations } from "../Shopping/data";

module.exports = df.orchestrator(function* (context) {
  const entityId = new df.EntityId("ShoppingEntity", "shoppingCart");

  yield context.df.callEntity(entityId, Operations.ADD_ITEM, {
    itemId: items[0].itemId,
    quantity: 1,
  });
  yield context.df.callEntity(entityId, Operations.ADD_ITEM, {
    itemId: items[2].itemId,
    quantity: 2,
  });
  yield context.df.callEntity(entityId, Operations.ADD_ITEM, {
    itemId: items[1].itemId,
    quantity: 10,
  });

  const cartValue: number = yield context.df.callEntity(
    entityId,
    Operations.GET_VALUE
  );

  console.log(cartValue);
});

ShoppingCart class definition:

export class ShoppingCart {
    #items: CartItem[];

    constructor() {
        this.#items = [];
    }

    public addItem(item: CartItem): void {
        this.#items.push(item);
    }

    public remoteItem(itemId: string): void {
        const index = this.#items.findIndex((item) => item.itemId === itemId);
        this.#items = this.#items.slice(index, index + 1);
    }

    public getCartValue(): number {
        return this.#items.reduce(
            (value, item) =>
                value + item.quantity * items.find((i) => i.itemId === item.itemId).price,
            0
        );
    }
}

Attempting to call the above ShoppingOrchestration orchestration actually doesn't work, and fails because cart.addItem() isn't a function (it is lost after serialization/deserialization). For obvious reasons, entity states can only be JSON-serializable values, since they have to be serialized/deserialized to be written to storage.

What I would recommend:

  1. Make sure we clearly document this limitation about entities
  2. Remove the shopping sample from our repo.

Alternatively, we could try to think about how we could make a rich class use-case work. For example, we could have a way for a user to provide a class constructor to the state initializer, and call that constructor every time we retrieve the state. This may still not cover every use-case though. More investigation and design would be required to see what makes sense here and if it's worth it.


Metadata

Metadata

Assignees

No one assigned

    Labels

    Needs: Investigation 🔍A deeper investigation needs to be done by the project maintainers.bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions