Azure / azure-functions-durable-js

JavaScript library for using the Durable Functions bindings
https://www.npmjs.com/package/durable-functions
MIT License
128 stars 47 forks source link

Entities do not support non-JSON serializable states #437

Open hossam-nasr opened 1 year ago

hossam-nasr commented 1 year ago

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.


hossam-nasr commented 1 year ago

Discussed offline. Let's make sure to update the docs here: https://learn.microsoft.com/en-us/azure/azure-functions/durable/durable-functions-serialization-and-persistence?tabs=javascript to make it clearer this isn't a supported scenario, and let's discuss whether this is a feature we want to invest time in. For now, doesn't seem pressing/related to v3 preview