Creating an API Module Project

Use the apimodule project template to create an API Module project.

Background

In this example, we will use the ScaleOut API Module Package template to create and customize a custom module that implements a simple shopping cart. The API processor in an API module is responsible for persisting, analyzing, and reacting to API module invocations.

Prerequisites

Procedure

1. Create an API module project

In Visual Studio’s New Project dialog, select the ScaleOut API Module Package project type and name it ShoppingCart.

If you are using Linux (or you prefer using the command line to create your project), run the following command to create a project based on the template:

dotnet new apimodule -n ShoppingCart

A new directory called “ShoppingCart” will be created under your current directory.

2. Customize state object

Customize the SOSS object used to store data for your module. A skeleton class named “ShoppingCartSossObject” was precreated by the template and must be modified to hold state for the module (in this case, a list of items in the shopping cart). Instances of this object will be stored in the ScaleOut StateServer cluster.

namespace ShoppingCart
{
    public record CartItem(string ProductId, int Quantity, decimal Price);

    /// <summary>
    /// Defines the object stored in the ScaleOut StateServer (SOSS) service 
    /// that hold state for the API module.
    /// </summary>
    public class ShoppingCartSossObject
    {
        public required List<CartItem> CartItems { get; init; }

        public required string Id { get; init; }
    }
}
  • SOSS objects should also contain a string property named Id. While not strictly required, the Id property is used by the ScaleOut Active Caching UI to identify objects in the cluster.

3. Implement the CreateObject() method

Implement the CreateObject method in your API Processor. A skeleton class named “ShoppingCartApiProcessor” was precreated by the template and must be modified with a CreateObject() implementation. This method is called when an API invocation is received and the associated SOSS object does not yet exist.

/// <summary>
/// Factory method to create a new instance of a shopping cart for this module.
/// </summary>
/// <param name="objectId">ID of the object in the ScaleOut service.</param>
/// <param name="moduleName">Name of your API module</param>
/// <returns>CreateResult instance, containing your object and its expiration policy.</returns>
public override CreateResult<ShoppingCartSossObject> CreateObject(string objectId, string moduleName)
{
    var newSossObj = new ShoppingCartSossObject
    {
        Id = objectId,
        CartItems = new List<CartItem>()
    };

    return new CreateResult<ShoppingCartSossObject>
    {
        Value = newSossObj,
        Expiration = TimeSpan.Zero, // No expiration
        ExpirationType = ExpirationType.Sliding
    };
}
  • The CreateResult instance returned by the CreateObject() method should have the following three properties set:

    • Value: The newly created SOSS object (in this case, a shopping cart whose Items property is an empty list).

    • Expiration: A TimeSpan indicating how long the object should be kept in the cluster before being automatically removed.

    • ExpirationType: An enum value indicating the expiration behavior for the object. Set to Sliding to reset the expiration every time the SossObject is accessed, or Absolute to keep it for a fixed duration.

4. Implement the API module’s endpoints

The API module’s endpoints are implemented in the ShoppingCartApiProcessor class, decorated with a [SossApiMethod] attribute. This class contains methods for handling incoming API requests and interacting with the SOSS objects.

Endpoints accept an arbitrary argument value as a byte array and can return a byte array as a response.

[SossApiMethod(OperationId = "AddItem",
               LockingMode = ApiProcessorLockingMode.ExclusiveLock)]
public Task<InvokeResult> AddItem(ApiProcessingContext<ShoppingCartSossObject> context,
                                   ShoppingCartSossObject sossObject,
                                   byte[] args)
{
    CartItem? newItem = System.Text.Json.JsonSerializer.Deserialize<CartItem>(args);
    if (newItem != null)
        sossObject.CartItems.Add(newItem);

    var invokeResult = new InvokeResult
    {
        ResultBytes = null,
        ProcessingResult = ProcessingResult.DoUpdate
    };
    return Task.FromResult(invokeResult);
}

[SossApiMethod(OperationId = "RemoveItem",
               LockingMode = ApiProcessorLockingMode.ExclusiveLock)]
public Task<InvokeResult> RemoveItem(ApiProcessingContext<ShoppingCartSossObject> context,
                                   ShoppingCartSossObject sossObject,
                                   byte[] args)
{
    string productId = System.Text.Json.JsonSerializer.Deserialize<string>(args) ?? string.Empty;
    var itemToRemove = sossObject.CartItems.FirstOrDefault(item => item.ProductId == productId);
    if (itemToRemove != null)
        sossObject.CartItems.Remove(itemToRemove);

    var invokeResult = new InvokeResult
    {
        ResultBytes = null,
        ProcessingResult = ProcessingResult.DoUpdate
    };
    return Task.FromResult(invokeResult);
}

[SossApiMethod(OperationId = "GetTotal", 
               LockingMode = ApiProcessorLockingMode.None)]
public Task<InvokeResult> GetTotal(ApiProcessingContext<ShoppingCartSossObject> context,
                                   ShoppingCartSossObject sossObject,
                                   byte[] args)
{
    decimal total = sossObject.CartItems.Sum(item => item.Quantity * item.Price);
    byte[] totalBytes = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(total);

    var invokeResult = new InvokeResult
    {
        ResultBytes = totalBytes,
        ProcessingResult = ProcessingResult.NoUpdate
    };
    return Task.FromResult(invokeResult);
}
  • Endpoints must be marked with the [SossApiMethod] attribute, indicating the string name of the API method and its locking behavior.

  • Use ApiProcessorLockingMode.ExclusiveLock in the [SossApiMethod] attribute if you need to ensure that only one request can modify the SOSS object at a time.

  • API endpoints must return an InvokeResult object. The following properties should be set:

  • ResultBytes: The byte array to return to the caller (if any).

  • ProcessingResult: Indicates what should be done with the object in the ScaleOut service after the API method has returned:

    • DoUpdate: Indicates that the object was (or may have been) modified and must be updated in the ScaleOut service.

    • NoUpdate: Indicates the object was not modified and does not need to be updated in the ScaleOut service. (If you are unsure of whether the object was modified, always return DoUpdate.)

    • Remove: Remove the associated SossObject from the ScaleOut cluster.

5. Publish the project

In Visual Studio, right-click on the ShoppingCart project in Visual Studio’s solution explorer and select “Publish”. The publishing window will allow you to select from several predefined targets (Windows vs. Linux). Click the “Publish” button to create a zipped deployment package.

Use the ScaleOut Active Caching UI to upload the package and manage your deployment. Once deployed, your package will run as a worker process on every host in the ScaleOut StateServer cluster.