Table of Contents

Creating an Invocation Grid Project

An Invocation Grid project template gives you a new project containing all the code and libraries needed to run an Invocation Grid worker process on a cluster of ScaleOut hosts. Use the project template to create an Invocation Grid project and then modify it with your own custom event handler code.

A full, runnable version of this example is available on GitHub: https://github.com/scaleoutsoftware/DotNetIgSample

Background

In this example, the ScaleOut service contains shopping cart objects. We want to find the total sales value of all backordered items that are currently in users' shopping carts. A PMI Reduce operation will be implemented to generate this value, and the code will be hosted in an Invocation Grid.

Prerequisites

Procedure

  1. Determine the class to be analyzed.

    [MessagePackObject]
    public class Cart
    {
        [Key(0)]
        public string UserId { get; set; }
    
        [Key(1)]
        public List<CartItem> Items { get; set; }
    }
    
    [MessagePackObject]
    public class CartItem
    {
        [Key(0)]
        public string ProductName { get; set; }
    
        [Key(1)]
        public int Quantity { get; set; }
    
        [Key(2)]
        public decimal Price { get; set; }
    
        [Key(3)]
        public bool Backordered { get; set; }
    }
    
    • The shopping cart class used in this example would typically be defined in a shared class library since both IG workers and ordinary client applications may need to reference it.

    • The classes here are defined to support MessagePack serialization. Using a custom serializer such as MessagePack or protobuf improves cache performance.

  2. From the command line or Visual Studio's "New Project" dialog, create a new Invocation Grid worker project named "ShoppingCartIG".

    dotnet new igworker -n ShoppingCartIG
    
  3. In the new ShoppingCartIG project, delete the MyInvokeHandler.cs demonstration class.

  4. Add a new PMI Reduce handler class that will analyze shopping carts in the ScaleOut service.

    /// <summary>
    /// PMI Reduce handler class that finds the total value of backordered
    /// items in user shopping carts, expressed as a decimal.
    /// </summary>
    class TotalBackorderedValue : Reduce<string, Cart, decimal>
    {
        readonly ILogger _logger;
    
        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="logger">ILogger instance.</param>
        public TotalBackorderedValue(ILogger logger)
        {
            _logger = logger;
        }
    
        /// <summary>
        /// Initializes a thread-local result accumulator.
        /// </summary>
        /// <returns>Decimal value of zero.</returns>
        public override decimal AccumulatorFactory() => decimal.Zero;
    
    
        /// <summary>
        /// Evaluates a shopping cart, adding the value of a cart's backordered items
        /// to the accumulated result.
        /// </summary>
        /// <param name="key">Key to the shopping cart object in the ScaleOut cache.</param>
        /// <param name="accumulator">Thread-local accumulated result value.</param>
        /// <param name="context">Context of the invoke operation</param>
        /// <returns>Accumulated value of backordered shopping cart items.</returns>
        public override decimal Evaluate(string key,
                                         decimal accumulator,
                                         OperationContext<string, Cart> context)
        {
            // Use the cache in the PMI context to retrieve the object being evaluated:
            var readOpt = new ReadOptions() { FastReadVersion = context.FastReadVersion };
            var readResponse = context.Cache.Read(key, readOpt);
    
            switch (readResponse.Result)
            {
                case ServerResult.Retrieved:
                    var cart = readResponse.Value;
    
                    // Perform analysis.
                    decimal backorderedVal = (from item in cart.Items
                                              where item.Backordered == true
                                              select item.Price * item.Quantity)
                                             .Sum();
    
                    return accumulator + backorderedVal;
                case ServerResult.NotFound:
                    _logger.LogInformation("{key} removed by another client.", key);
                    return accumulator;
                default:
                    // Throw an exception here if you'd like to return
                    // UnhandledExceptionInCallback to the Cache.Invoke caller.
                    _logger.LogWarning("Unexpected error {result} reading {key}.", 
                                       readResponse.Result, key);
                    return accumulator;
            }
        }
    
        /// <summary>
        /// Combines accumulated result objects from different threads/machines.
        /// </summary>
        /// <param name="result1">First result.</param>
        /// <param name="result2">Second result.</param>
        /// <returns>Merge result.</returns>
        public override decimal MergeFinal(decimal result1, decimal result2) =>
            result1 + result2;
    
        /// <summary>
        /// Deserializes a result object using MessagePack.
        /// </summary>
        /// <param name="stream">Stream containing serialized result.</param>
        /// <returns>Decimal.</returns>
        public override decimal DeserializeResult(Stream stream) =>
            MessagePackSerializer.Deserialize<decimal>(stream);
    
        /// <summary>
        /// Serializes a result to a stream using MessagePack.
        /// </summary>
        /// <param name="result">Decimal result.</param>
        /// <param name="stream">Stream to write the serialized result to.</param>
        public override void SerializeResult(decimal result, Stream stream) =>
            MessagePackSerializer.Serialize(stream, result);
    }
    
  5. Modify the Configure() method in the ShoppingCartIG project's Startup.cs file to configure the shopping cart cache and register the new TotalBackorderedValue PMI handler class.

    /// <summary>
    /// Configures an invocation grid when it first starts up. Typically used to
    /// initialize Scaleout.Client caches and register invocation handlers.
    /// </summary>
    public void Configure(GridConnection gridConnection, 
                          ILogger logger, 
                          byte[] startupParam, 
                          string igName)
    {
        // Configure the cache use to access shopping cart objects.
        var cacheBuilder = new CacheBuilder<string, Cart>("Shopping Carts", gridConnection);
    
        cacheBuilder.SetSerialization(
                      (cart, stream) => MessagePackSerializer.Serialize(stream, cart),
                      (stream) => MessagePackSerializer.Deserialize<Cart>(stream));
    
        cacheBuilder.SetClientCache("Random-MaxMemory", capacity: 1000, partitionCount: 0);
        cacheBuilder.SetKeystringCacheSize(100_000);
    
        var cache = cacheBuilder.Build();
    
        // Register the TotalBackorderedValue PMI handler class.
        ServiceEvents.SetInvokeHandler(cache, new TotalBackorderedValue(logger));
    }
    

Result

The ShoppingCartIG Invocation Grid worker project is ready to be built and deployed to hosts running the ScaleOut service, at which point it can handle "TotalBackorderedValue" Invoke requests from clients.

Next, use the IG command-line utility to package and deploy your project to ScaleOut hosts.