Cyclicity
One of the design requirements of Avalanche.Service library has been the capability to process cyclic requests and produce cyclic results. The requirement affected many aspects and needed to be addressed throughout the library. Cyclicity is a use case that is not used often, but when it is, it must be integral part of the design as it cannot be added afterwards.
An object graph that has circular chain of references is cyclic.
Handler implementation
The library makes it easy to break down complicated cyclic requests into easily processable simple handlers.
There are however a few special considerations that handler implementation must adhere to:
- Result objects must be reference objects
- Result objects must be constructed in two phases. Initial reference must be assigned after construction.
- Cache must be used for the request type.
- Other handlers must get reference with .GetInitialized()
- One handler must provide final reference with .SetValue to indicate result is completed.
// Choose cache policy
IRequestPolicy<CachePolicy> cachePolicy = CachePolicies.ToCache;
// Create service
IService<Node, int> countService = Services.Create<Node, int>(new NodeCounter(), cachePolicy);
// Create nodes
Node root = new(), node1 = new(), node2 = new(), node3 = new();
// Cyclic graph
root.Children = new Node[] { node1, node2, node3, root };
// Count number of nodes
int count = countService.GetRequired(root);
public class Node
{
/// <summary>Forward reference edge</summary>
public Node[]? Children;
}
public class NodeCounter : IHandler<Node, int>
{
public void Handle(IQuery<Node, int> query)
{
// Already handled
if (query.Handled()) return;
// Assign initial value (required)
query.Response.SetValue(0);
// Start counting, include self
int count = 1;
// Get node
Node node = query.Request;
// Count children
if (node.Children != null)
{
foreach (Node child in node.Children)
{
// Count nodes
query.Service.TryGetInitialized<Node, int>(child, out int childCount);
// Add up
count += childCount;
}
}
// Assign new value
query.Response.SetValue(count);
}
}
Query handler cycle may occur if rules are not followed. The most common cause is that request is non-cached. To conserve CPU cycles and memory, the library does not detect perform detection of cyclicity of query handlers.
Full Example
Full example
using System;
using Avalanche.Service;
public class handler_cyclic
{
public static void Run()
{
// <1>
// Choose cache policy
IRequestPolicy<CachePolicy> cachePolicy = CachePolicies.ToCache;
// Create service
IService<Node, int> countService = Services.Create<Node, int>(new NodeCounter(), cachePolicy);
// Create nodes
Node root = new(), node1 = new(), node2 = new(), node3 = new();
// Cyclic graph
root.Children = new Node[] { node1, node2, node3, root };
// Count number of nodes
int count = countService.GetRequired(root);
// </1>
Console.WriteLine(count);
}
// <2>
public class Node
{
/// <summary>Forward reference edge</summary>
public Node[]? Children;
}
// </2>
// <3>
public class NodeCounter : IHandler<Node, int>
{
public void Handle(IQuery<Node, int> query)
{
// Already handled
if (query.Handled()) return;
// Assign initial value (required)
query.Response.SetValue(0);
// Start counting, include self
int count = 1;
// Get node
Node node = query.Request;
// Count children
if (node.Children != null)
{
foreach (Node child in node.Children)
{
// Count nodes
query.Service.TryGetInitialized<Node, int>(child, out int childCount);
// Add up
count += childCount;
}
}
// Assign new value
query.Response.SetValue(count);
}
}
// </3>
}