diff --git a/src/NRedisStack/Graph/DataTypes/Edge.cs b/src/NRedisStack/Graph/DataTypes/Edge.cs
new file mode 100644
index 00000000..24e7df5b
--- /dev/null
+++ b/src/NRedisStack/Graph/DataTypes/Edge.cs
@@ -0,0 +1,96 @@
+using System.Text;
+
+namespace NRedisStack.Graph.DataTypes
+{
+ ///
+ /// A class reprenting an edge (graph entity). In addition to the base class properties, an edge shows its source,
+ /// destination, and relationship type.
+ ///
+ public class Edge : GraphEntity
+ {
+ ///
+ /// The relationship type.
+ ///
+ ///
+ public string RelationshipType { get; set; }
+
+ ///
+ /// The ID of the source node.
+ ///
+ ///
+ public long Source { get; set; }
+
+ ///
+ /// The ID of the desination node.
+ ///
+ ///
+ public long Destination { get; set; }
+
+ ///
+ /// Overriden from the base `Equals` implementation. In addition to the expected behavior of checking
+ /// reference equality, we'll also fall back and check to see if the: Source, Destination, and RelationshipType
+ /// are equal.
+ ///
+ /// Another `Edge` object to compare to.
+ /// True if the two instances are equal, false if not.
+ public override bool Equals(object? obj)
+ {
+ if (obj == null) return this == null;
+
+ if (this == obj)
+ {
+ return true;
+ }
+
+ if (!(obj is Edge that))
+ {
+ return false;
+ }
+
+ if (!base.Equals(obj))
+ {
+ return false;
+ }
+
+ return Source == that.Source && Destination == that.Destination && RelationshipType == that.RelationshipType;
+ }
+
+ ///
+ /// Overriden from base to compute a deterministic hashcode based on RelationshipType, Source, and Destination.
+ ///
+ /// An integer representing the hash code for this instance.
+ public override int GetHashCode()
+ {
+ unchecked
+ {
+ int hash = 17;
+
+ hash = hash * 31 + base.GetHashCode();
+ hash = hash * 31 + RelationshipType.GetHashCode();
+ hash = hash * 31 + Source.GetHashCode();
+ hash = hash * 31 + Destination.GetHashCode();
+
+ return hash;
+ }
+ }
+
+ ///
+ /// Override from base to emit a string that contains: RelationshipType, Source, Destination, Id, and PropertyMap.
+ ///
+ /// A string containing a description of the Edge containing a RelationshipType, Source, Destination, Id, and PropertyMap.
+ public override string ToString()
+ {
+ var sb = new StringBuilder();
+
+ sb.Append("Edge{");
+ sb.Append($"relationshipType='{RelationshipType}'");
+ sb.Append($", source={Source}");
+ sb.Append($", destination={Destination}");
+ sb.Append($", id={Id}");
+ sb.Append($", {PropertyMapToString()}");
+ sb.Append("}");
+
+ return sb.ToString();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/NRedisStack/Graph/DataTypes/GraphEntity.cs b/src/NRedisStack/Graph/DataTypes/GraphEntity.cs
new file mode 100644
index 00000000..d902c06b
--- /dev/null
+++ b/src/NRedisStack/Graph/DataTypes/GraphEntity.cs
@@ -0,0 +1,89 @@
+using System.Text;
+
+namespace NRedisStack.Graph.DataTypes
+{
+ ///
+ /// An abstract representation of a graph entity.
+ /// A graph entity has an ID and a set of properties. The properties are mapped and accessed by their names.
+ ///
+ public abstract class GraphEntity
+ {
+ public long Id { get; set; }
+
+ public IDictionary PropertyMap = new Dictionary();
+
+ ///
+ /// Overriden Equals that considers the equality of the entity ID as well as the equality of the
+ /// properties that each entity has.
+ ///
+ ///
+ ///
+ public override bool Equals(object? obj)
+ {
+ if (obj == null) return this == null;
+
+ if (this == obj)
+ {
+ return true;
+ }
+
+ if (!(obj is GraphEntity that))
+ {
+ return false;
+ }
+
+ return Id == that.Id && (PropertyMap.SequenceEqual(that.PropertyMap));
+ }
+
+ ///
+ /// Overriden GetHashCode that computes a deterministic hash code based on the value of the ID
+ /// and the name/value of each of the associated properties.
+ ///
+ ///
+ public override int GetHashCode()
+ {
+ unchecked
+ {
+ int hash = 17;
+
+ hash = hash * 31 + Id.GetHashCode();
+
+ foreach(var prop in PropertyMap)
+ {
+ hash = hash * 31 + prop.Key.GetHashCode();
+ hash = hash * 31 + prop.Value.GetHashCode();
+ }
+
+ return hash;
+ }
+ }
+
+ ///
+ /// Overriden ToString that emits a string containing the ID and property map of the entity.
+ ///
+ ///
+ public override string ToString()
+ {
+ var sb = new StringBuilder();
+
+ sb.Append("GraphEntity{id=");
+ sb.Append(Id);
+ sb.Append(", propertyMap=");
+ sb.Append(PropertyMap);
+ sb.Append('}');
+
+ return sb.ToString();
+ }
+
+ public string PropertyMapToString()
+ {
+ var sb = new StringBuilder();
+
+ sb.Append("propertyMap={");
+ sb.Append(string.Join(", ", PropertyMap.Select(pm => $"{pm.Key}={pm.Value}")));
+ sb.Append("}");
+
+ return sb.ToString();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/NRedisStack/Graph/DataTypes/Node.cs b/src/NRedisStack/Graph/DataTypes/Node.cs
new file mode 100644
index 00000000..05a30e86
--- /dev/null
+++ b/src/NRedisStack/Graph/DataTypes/Node.cs
@@ -0,0 +1,86 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace NRedisStack.Graph.DataTypes
+{
+ ///
+ /// A class representing a node (graph entity). In addition to the base class ID and properties, a node has labels.
+ ///
+ public sealed class Node : GraphEntity
+ {
+ public List Labels { get; }
+
+ public Node()
+ {
+ Labels = new List();
+ }
+
+ ///
+ /// Overriden member that checks to see if the names of the labels of a node are equal
+ /// (in addition to base `Equals` functionality).
+ ///
+ ///
+ ///
+ public override bool Equals(object? obj)
+ {
+ if (obj == null) return this == null;
+
+ if (this == obj)
+ {
+ return true;
+ }
+
+ if (!(obj is Node that))
+ {
+ return false;
+ }
+
+ if (!base.Equals(obj))
+ {
+ return false;
+ }
+
+ return Enumerable.SequenceEqual(Labels, that.Labels);
+ }
+
+ ///
+ /// Overridden member that computes a hash code based on the base `GetHashCode` implementation
+ /// as well as the hash codes of all labels.
+ ///
+ ///
+ public override int GetHashCode()
+ {
+ unchecked
+ {
+ int hash = 17;
+
+ foreach(var label in Labels)
+ {
+ hash = hash * 31 + label.GetHashCode();
+ }
+
+ hash = hash * 31 + base.GetHashCode();
+
+ return hash;
+ }
+ }
+
+ ///
+ /// Overridden member that emits a string containing the labels, ID, and property map of a node.
+ ///
+ ///
+ public override string ToString()
+ {
+ var sb = new StringBuilder();
+
+ sb.Append("Node{labels=");
+ sb.Append($"[{string.Join(", ", Labels)}]");
+ sb.Append($", id={Id}");
+ sb.Append($", {PropertyMapToString()}");
+ sb.Append("}");
+
+ return sb.ToString();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/NRedisStack/Graph/DataTypes/Path.cs b/src/NRedisStack/Graph/DataTypes/Path.cs
new file mode 100644
index 00000000..d3f5a68c
--- /dev/null
+++ b/src/NRedisStack/Graph/DataTypes/Path.cs
@@ -0,0 +1,93 @@
+using System.Collections.ObjectModel;
+using System.Runtime.CompilerServices;
+using System.Text;
+
+[assembly: InternalsVisibleTo("NRedisStack.Tests.Graph")]
+
+namespace NRedisStack.Graph.DataTypes
+{
+ ///
+ /// This class represents a path in the graph.
+ ///
+ public class Path
+ {
+ public ReadOnlyCollection Nodes { get;}
+ public ReadOnlyCollection Edges { get;}
+
+ public Path(IList nodes, IList edges)
+ {
+ Nodes = new ReadOnlyCollection(nodes);
+ Edges = new ReadOnlyCollection(edges);
+ }
+
+
+ ///
+ /// How many edges exist on this path.
+ ///
+ public int Length => Edges.Count;
+
+ ///
+ /// Overriden `Equals` method that will consider the equality of the Nodes and Edges between two paths.
+ ///
+ ///
+ ///
+ public override bool Equals(object? obj)
+ {
+ if (obj == null) return this == null;
+
+ if (this == obj)
+ {
+ return true;
+ }
+
+ if (!(obj is Path path))
+ {
+ return false;
+ }
+
+ return Enumerable.SequenceEqual(Nodes, path.Nodes) && Enumerable.SequenceEqual(Edges, path.Edges);
+ }
+
+ ///
+ /// Overridden `GetHashCode` method that will compute a hash code using the hash code of each node and edge on
+ /// the path.
+ ///
+ ///
+ public override int GetHashCode()
+ {
+ unchecked
+ {
+ int hash = 17;
+
+ foreach (var node in Nodes)
+ {
+ hash = hash * 31 + node.GetHashCode();
+ }
+
+ foreach (var edge in Edges)
+ {
+ hash = hash * 31 + edge.GetHashCode();
+ }
+
+ return hash;
+ }
+ }
+
+ ///
+ /// Overridden `ToString` method that will emit a string based on the string values of the nodes and edges
+ /// on the path.
+ ///
+ ///
+ public override string ToString()
+ {
+ var sb = new StringBuilder();
+
+ sb.Append("Path{");
+ sb.Append($"nodes={Nodes}");
+ sb.Append($", edges={Edges}");
+ sb.Append("}");
+
+ return sb.ToString();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/NRedisStack/Graph/GraphCache.cs b/src/NRedisStack/Graph/GraphCache.cs
new file mode 100644
index 00000000..d8867615
--- /dev/null
+++ b/src/NRedisStack/Graph/GraphCache.cs
@@ -0,0 +1,23 @@
+namespace NRedisStack.Graph
+{
+ internal sealed class GraphCache
+ {
+ public GraphCacheList Labels { get; set; }
+ public GraphCacheList PropertyNames { get; set; }
+ public GraphCacheList RelationshipTypes { get; set; }
+
+ public GraphCache(string graphName, GraphCommands redisGraph)
+ {
+ Labels = new GraphCacheList(graphName, "db.labels", redisGraph);
+ PropertyNames = new GraphCacheList(graphName, "db.propertyKeys", redisGraph);
+ RelationshipTypes = new GraphCacheList(graphName, "db.relationshipTypes", redisGraph);
+ }
+
+ public string GetLabel(int index) => Labels.GetCachedData(index);
+
+ public string GetRelationshipType(int index) => RelationshipTypes.GetCachedData(index);
+
+ public string GetPropertyName(int index) => PropertyNames.GetCachedData(index);
+
+ }
+}
\ No newline at end of file
diff --git a/src/NRedisStack/Graph/GraphCacheList.cs b/src/NRedisStack/Graph/GraphCacheList.cs
new file mode 100644
index 00000000..74db8abe
--- /dev/null
+++ b/src/NRedisStack/Graph/GraphCacheList.cs
@@ -0,0 +1,66 @@
+namespace NRedisStack.Graph
+{
+ internal class GraphCacheList
+ {
+ protected readonly string GraphName;
+ protected readonly string Procedure;
+ private string[] _data;
+
+ protected readonly GraphCommands graph;
+
+ private readonly object _locker = new object();
+
+
+ internal GraphCacheList(string graphName, string procedure, GraphCommands redisGraph)
+ {
+ GraphName = graphName;
+ Procedure = procedure;
+ graph = redisGraph;
+ }
+
+ // TODO: Change this to use Lazy?
+ internal string GetCachedData(int index)
+ {
+ if (_data == null || index >= _data.Length)
+ {
+ lock(_locker)
+ {
+ if (_data == null || index >= _data.Length)
+ {
+ GetProcedureInfo();
+ }
+ }
+ }
+
+ return _data.ElementAtOrDefault(index);
+ }
+
+ private void GetProcedureInfo()
+ {
+ var resultSet = CallProcedure();
+ var newData = new string[resultSet.Count];
+ var i = 0;
+
+ foreach (var record in resultSet)
+ {
+ newData[i++] = record.GetString(0);
+ }
+
+ _data = newData;
+ }
+
+ protected virtual ResultSet CallProcedure() =>
+ graph.CallProcedure(GraphName, Procedure);
+ }
+
+ internal class ReadOnlyGraphCacheList : GraphCacheList
+ {
+ internal ReadOnlyGraphCacheList(string graphName, string procedure, GraphCommands redisGraph) :
+ base(graphName, procedure, redisGraph)
+ {
+ }
+
+ protected override ResultSet CallProcedure() =>
+ graph.CallProcedureReadOnly(GraphName, Procedure);
+ }
+}
\ No newline at end of file
diff --git a/src/NRedisStack/Graph/GraphCommands.cs b/src/NRedisStack/Graph/GraphCommands.cs
new file mode 100644
index 00000000..f7e83174
--- /dev/null
+++ b/src/NRedisStack/Graph/GraphCommands.cs
@@ -0,0 +1,469 @@
+using System.Text;
+using NRedisStack.Graph;
+using NRedisStack.Literals;
+using StackExchange.Redis;
+using static NRedisStack.Graph.RedisGraphUtilities;
+
+
+namespace NRedisStack
+{
+
+ public class GraphCommands : IGraphCommands
+ {
+ IDatabase _db;
+
+ public GraphCommands(IDatabase db)
+ {
+ _db = db;
+ }
+
+ internal static readonly object CompactQueryFlag = "--COMPACT";
+
+ private readonly IDictionary _graphCaches = new Dictionary();
+
+ private GraphCache GetGraphCache(string graphName)
+ {
+ if (!_graphCaches.ContainsKey(graphName))
+ {
+ _graphCaches.Add(graphName, new GraphCache(graphName, this));
+ }
+
+ return _graphCaches[graphName];
+ }
+
+ ///
+ /// Execute a Cypher query with parameters.
+ ///
+ /// A graph to perform the query on.
+ /// The Cypher query.
+ /// Parameters map.
+ /// Timeout (optional).
+ /// A result set.
+ ///
+ public ResultSet Query(string graphName, string query, IDictionary parameters, long? timeout = null)
+ {
+ var preparedQuery = PrepareQuery(query, parameters);
+
+ return Query(graphName, preparedQuery, timeout);
+ }
+
+ ///
+ /// Execute a Cypher query with parameters.
+ ///
+ /// A graph to perform the query on.
+ /// The Cypher query.
+ /// Parameters map.
+ /// Timeout (optional).
+ /// A result set.
+ ///
+ public async Task QueryAsync(string graphName, string query, IDictionary parameters, long? timeout = null)
+ {
+ var preparedQuery = PrepareQuery(query, parameters);
+
+ return await QueryAsync(graphName, preparedQuery, timeout);
+ }
+
+ ///
+ /// Execute a Cypher query.
+ ///
+ /// A graph to perform the query on.
+ /// The Cypher query.
+ /// Timeout (optional).
+ /// A result set.
+ ///
+ public ResultSet Query(string graphName, string query, long? timeout = null)
+ {
+ if(!_graphCaches.ContainsKey(graphName))
+ {
+ _graphCaches.Add(graphName, new GraphCache(graphName, this));
+ }
+
+ var args = (timeout == null) ? new List