ReactGraph Part 3 – The Data Model

Investigation into bringing together graph and reactive computing. This is the third post in the series and deals with the platform’s data model.

Nodes and Edges

Everything in ReactGraph is a node. A node has an identity and stores a value. An edge is also a node, which means it too has identity and can store a value. Furthermore, an edge describes a directed path from a source to a destination node. The edge also has a predicate that gives some meaning to the path. An edge can be marked as “bidirectional”, which means that it also represents the destination-to-source reverse path.

The above shouldn’t be of surprise to anyone who has dealt with the representation of information using graphs. The Neo4j property graph data model is very similar (with the exception of “an edge is also a node” characteristic). Facebook’s graph data model also exhibits similar properties to that of Neo4j.

public interface INode
{
   string Id { get; }
   T Value<T>();
}

public interface IEdge : INode
{
   string SourceId { get; }
   string DestinationId { get; }
   string Predicate { get; }
   bool IsBidirectional { get; }
}

Why treat edges as nodes?

So that the can act as the source of destination of other edges. This way there is no need to reify a relationship, if the application designers don’t wish to do so. The data model is flexible. For a discussion on this see my post from 2013 entitled “On Graph Data Model Design – Relationships”.

Here’s an example I just discussed with Jim. There are multiple ways to model the data in an application of domain. One might decide to reify relationships by creating extra nodes. The best approach is the one that makes sense for the specific application.

(id: jim) -> (id: foo, predicate: watched, on: 10/10/17) -> (id: SW-IV, title: "StarWars Episode IV")
(id: savas) -> (id: bar1, predicate: liked) -> (id: foo)
(id: savas) -> (id: bar1, predicate: shared, on: 10/11/17) -> (id: foo)

Graph Queries and Reactive Computing

With the graph data representation in place, we move to the basic abstractions necessary to support both “pull” graph querying and “push” continuous querying (reactive computing). The interface for a node is expanded with four additional properties.

public interface INode
{
   string Id { get; }
   T Value<T>();

   IQueryable<IEdge> IncomingEdges { get; }
   IQueryable<IEdge> OutgoingEdges { get; }

   IQbservable<IValueEvent<INode>> ValueChanges { get; }
   IQbservable<IEdgeEvent> EdgeChanges { get; }
}

The IncomingEdges and OutgoingEdges let us create query expressions that can be submitted for evaluation to the data provider supporting the respective IQueryable interfaces. The properties represent the collection of incoming and outgoing edges respectively.

The ValueChanges property allows the construction of Rx expression trees that can be submitted for continuous evaluation at the reactive computing data provider supporting the implementation of the INode interface. The ValueChanges represents the stream of changes to the value that the INode holds.

Similarly, the EdgeChanges allows the construction of Rx expression trees that can be submitted for continuous evaluation at the reactive computing data provider supporting the implementation of the INode interface. The EdgeChanges represents the stream of changes to the incoming and outgoing edges (e.g. when a new edge is created or deleted with the specific INode as a source of destination).

Please note that it’s an open design issue whether these properties should really be part of the INode interface. They can easily be provided as extension methods on INode by an implementor.

Graph operations

One could interact with the ReactGraph active computing store through an implementation of the following interface.

public interface IGraph
{
   IQueryable<INode> Nodes { get; }
   IQueryable<IEdge> Edges { get; }

   IQbservable<IValueEvent<INode>> ValueChanges { get; }
   IQbservable<IEdgeEvent> EdgeChanges { get; }

   Task<INode> GetNode(string id);
   Task<IEdge> GetEdge(string id);

   Task<INode> NewNode<T>(string id, T value = default(T));
   Task<IEdge> NewEdge<T>(
      string id,
      string sourceId,
      string predicate,
      string destinationId,
      T value = default(T),
      bool bidirectional = false);

   Task Update<T>(string id, T value);

   Task Delete(string id);
}

The interface should be self-explanatory.

The final INode interface

During the implementation stage of the interfaces, it became aparent that INode could use a couple of more things. Again, it’s an open design issue whether these should be part of the core interface.

public interface INode
{
   string Id { get; }

   T Value<T>();
   Type ValueType { get; }

   IGraph Graph { get; }

   IQueryable<IEdge> IncomingEdges { get; }
   IQueryable<IEdge> OutgoingEdges { get; }

   IQbservable<IValueEvent<INode>> ValueChanges { get; }
   IQbservable<IEdgeEvent> EdgeChanges { get; }
}

Miscellaneous

Finally, for completeness, here are the event-related interfaces. It’s an open design issue whether these two interfaces could be consolidated somehow.

public interface IValueEvent<out T>
{
   ChangeOperation Operation { get; }
   T Item { get; }
}
public interface IEdgeEvent
{
   INode Node { get; }
   IEdge Edge { get; }
   ChangeOperation Operation { get; }
}
public enum ChangeOperation
{
   New,
   Delete,
   Update,
}

With the basic abstractions in place, we are now ready to start exploring what we can do with these. Then we will move to the implementation of the “(re)active graph store”.

I will try to make the code available on github as soon as possible.

Next: ReactGraph Part 4 – First (Re)Active Queries