This chapter's example uses C# to build a NoSQL graph database in the cloud. It uses the Neo4j AuraDB database to build and perform queries on the org chart shown in Figure 21.1.
If you skipped Chapter 20, “Neo4j AuraDB in Python,” which built a similar example in Python, go to that chapter and read the beginning and the first three sections, which are described in the following list.
When you reach the section “Create the Program” in Chapter 20, return to this chapter and read the following sections.
To create a C# program to work with the AuraDB org chart database, create a new C# Console App (.NET Framework) and then add the code described in the following sections.
Like the example described in Chapter 20, this example builds an assortment of helper methods that do things like create nodes, build relationships, and execute queries on the data. It then uses those methods to create two higher-level methods that build and query the org chart. Finally, the main program connects to the database and calls those two methods.
This example uses the Neo4j driver to allow your program to communicate with the AuraDB database sitting in the cloud. To add the driver to your project, follow these steps:
This example uses a pattern that is slightly different from the one used in Chapter 20. The previous example used action methods that took a transaction object as a parameter and then used that object's methods to do the work.
In this example, the helper methods take a database driver as a parameter. Those methods use the driver's Session
method to get a session object that they then use to run database commands.
Session objects can start transactions that you can commit or roll back. This example simply calls the session's Run
method to execute database commands, and the Run
method uses an auto-commit transaction by default so that we don't need to worry about committing the result.
All of these features are differences in the two database adapters, but the underlying database engine is the same. You will often find multiple database adapters available for a given database engine, so you might want to shop around to find one that provides features that you like.
The session objects used in this example are “simple” sessions that work synchronously. Neo4j also provides asynchronous session objects that may be useful for some applications, particularly if your network is slow. We're using the “simple” sessions because they are, if not exactly simple, at least simpler than the asynchronous version.
The following sections divide the program's code into three parts: action methods that build basic objects such as nodes and relationships, org chart methods that build and explore the org chart, and the main program.
The following sections describe the lower-level action methods. Each takes a driver object as its first parameter. They use the driver to create a session object, and then use that object to run a database command or query.
They follow the same pattern, so you'll probably get the hang of them quickly. The more interesting differences are the query commands that they send to the database. Those queries are vaguely reminiscent of SQL, but they're not the same because they need to work with a graph database instead of a relational database.
Table 21.1 lists the action methods and gives their purposes.
TABLE 21.1: Action methods and their purposes
METHOD | PURPOSE |
---|---|
DeleteAllNodes | Delete all nodes and their links. |
MakeNode | Create a new node. |
MakeLink | Create a relationship between two nodes. |
ExecuteNodeQuery | Execute a query that returns nodes. |
FindPath | Find a path between two nodes. |
The following code shows how the DeleteAllNodes
method deletes all the nodes in the database:
using Neo4j.Driver;
…
// Delete all previous nodes and links.
static void DeleteAllNodes(IDriver driver)
{
using (ISession session = driver.Session())
{
string statement = "MATCH (n) DETACH DELETE n";
session.Run(statement);
}
}
The module includes the statement using Neo4j.Driver
to make it easier to use the driver.
The DeleteAllNodes
method takes a driver object as a parameter, uses it to create a session object, and then uses the session
object's Run
method to execute the database command MATCH (n) DETACH DELETE n
.
Notice that the session object is created in a using
block, so it is automatically disposed of when we're done with it. The other methods in this example use a similar approach. Alternatively, you could create a single session and pass it around to all the methods.
The MATCH
command is similar to a SQL query. It uses a pattern to match objects inside the database. Then later parts of the command do something with any objects that match.
In this case, the pattern (n)
matches any node. In general, expressions inside parentheses ()
match nodes and expressions surrounded by square brackets []
match relationships.
Note that you cannot delete a node if it is involved in any relationships. (This is kind of like a graph database version of a foreign key constraint.) You can either delete the relationships first or include the DETACH
keyword to tell the database to delete those relationships automatically with the node.
The last part of the command is DELETE n
, which deletes the node n
. Here, n
is a node that was matched by the MATCH (n)
part of the command.
To summarize, this statement says, “Match all nodes, detach their relationships, and then delete them.”
The following code shows the MakeNode
method:
// Use parameters to make a node.
static void MakeNode(IDriver driver, string id, string title)
{
using (ISession session = driver.Session())
{
string statement = "CREATE (n:OrgNode { ID:$id, Title:$title })";
Dictionary<string, object> parameters =
new Dictionary<string, object>()
{
{"id", id},
{"title", title},
};
session.Run(statement, parameters);
}
}
This method creates a session and then composes a CREATE
command to add a node to the database. The n:OrgNode
part tells the database that this node's type is OrgNode
. That's just a name that I made up for this kind of node. You could create CustomerNode
, RecipeNode
, SaberToothDuckNode
, or any node types that make sense for your application. You also don't need to include the word Node
; I just decided that it would make the queries easier to understand.
The part of the statement that's inside the curly brackets tells the database to give the node two properties, ID
and Title
. We'll provide values for the placeholders with the dollar signs shortly.
Note that property names are case-sensitive, so if you create a Title
property and later search for a title
value, you won't get any matches. (And you'll waste a lot of time wondering why not.)
The code then makes a dictionary that uses the placeholder names as keys and supplies values for them. The first dictionary entry associates the $id
placeholder with the parameter id
that was passed into MakeNode
. The second entry associates the $title
placeholder with the method's title
parameter.
The code finishes by calling the session
object's Run
method, passing it the command string and the parameters
dictionary.
The following code shows how the MakeLink
method creates a relationship between two nodes:
// Use parameters to create a link between an OrgNode and its parent.
static void MakeLink(IDriver driver, string id, string boss)
{
using (ISession session = driver.Session())
{
string statement =
"MATCH" +
" (a:OrgNode)," +
" (b:OrgNode) " +
"WHERE" +
" a.ID = $id AND " +
" b.ID = $boss " +
"CREATE (a)-[r:REPORTS_TO { Name:a.ID + '->' + b.ID }]->(b) ";
Dictionary<string, object> parameters =
new Dictionary<string, object>()
{
{"id", id},
{"boss", boss},
};
session.Run(statement, parameters);
}
}
This method creates a REPORTS_TO
link for the org chart. The MATCH
statement looks for two OrgNode
objects, a
and b
, where a.ID
matches the id
parameter passed into the method and b.ID
matches the boss
parameter.
After the MATCH
statement finds those two nodes, the CREATE
command makes a new REPORTS_TO
relationship leading from node a
to node b
. It gives the relationship a Name
property that contains the two IDs separated by ->
. For example, if the nodes' IDs are A
and B
, then this relationship's Name
is A->B
.
The following code shows the ExecuteNodeQuery
method, which executes a query that matches nodes that are named n
in the query. This method returns a list of the query's results:
// Execute a query that returns zero or more nodes
// identified by the name "n".
// Return the nodes in the format "ID: Title".
static List<string> ExecuteNodeQuery(IDriver driver, string query)
{
List<string> result = new List<string>();
using (ISession session = driver.Session())
{
foreach (IRecord record in session.Run(query))
{
INode node = (INode)record.Values["n"];
result.Add($"{node["ID"]}: {node["Title"]}");
}
}
return result;
}
The method first creates a list to hold results. It then runs the query and loops through the records that it returns.
Inside the loop, it uses record.Values["n"]
to get the result named n
from the current record. That result will be a node selected by the MATCH
statement. (You'll see that kind of MATCH
statement a bit later.) The code copies the node's ID
and Title
properties into a string and adds the string to the result list.
After it finishes looping through the records, the method returns the list.
The following code shows how the FindPath
method finds a path between two nodes in the org chart:
// Execute a query that returns a path from node id1 to node id2.
// Return the nodes in the format "ID: Title".
static List<string> FindPath(IDriver driver, string id1, string id2)
{
List<string> result = new List<string>();
using (ISession session = driver.Session())
{
string statement =
"MATCH" +
" (start:OrgNode { ID:$id1 } )," +
" (end:OrgNode { ID:$id2 } ), " +
" p = shortestPath((start)-[:REPORTS_TO *]-(end)) " +
"RETURN p";
Dictionary<string, object> parameters =
new Dictionary<string, object>()
{
{"id1", id1},
{"id2", id2},
};
// Get one path.
IRecord record = session.Run(statement, parameters).Single();
IPath path = (IPath)record.Values["p"];
foreach (INode node in path.Nodes)
{
result.Add($"{node["ID"]}: {node["Title"]}");
}
}
return result;
}
This code looks for two nodes that have the IDs passed into the method as parameters. It names the matched nodes start
and end
.
It then calls the database's shortestPath
method to find a shortest path from start
to end
following REPORTS_TO
relationships. The *
means the path can have any length. The statement saves the path that it found with the name p
.
Note that the shortestPath
method only counts the number of relationships that it crosses; it doesn't consider costs or weights on the relationships. In other words, it looks for a path with the fewest steps, not necessarily the shortest total cost as you would like in a street network, for example. Some AuraDB databases can perform the least total cost calculation and execute other graph algorithms, but the free version cannot.
After it composes the database command, the method executes it, passing in the necessary parameters. It calls the result's single
method to get the first returned result.
It then looks at that result's p
property, which holds the path. (Remember that the statement saved the path with the name p
.)
The code loops through the path's nodes and adds each one's ID
and Title
values to a result list. The method finishes by returning that list.
That's the end of the action methods. They do the following:
The following sections describe the two methods that use those tools to build and query the org chart. The earlier action methods make these two relatively straightforward.
The following code shows how the BuildOrgChart
method builds the org chart:
// Build the org chart.
static void BuildOrgChart(IDriver driver)
{
// Make the nodes.
MakeNode(driver, "A", "President");
MakeNode(driver, "B", "VP Ambiguity");
MakeNode(driver, "C", "VP Shtick");
MakeNode(driver, "D", "Dir Puns and Knock-Knock Jokes");
MakeNode(driver, "E", "Dir Riddles");
MakeNode(driver, "F", "Mgr Pie and Food Gags");
MakeNode(driver, "G", "Dir Physical Humor");
MakeNode(driver, "H", "Mgr Pratfalls");
MakeNode(driver, "I", "Dir Sight Gags");
// Make the links.
MakeLink(driver, "B", "A");
MakeLink(driver, "C", "A");
MakeLink(driver, "D", "B");
MakeLink(driver, "E", "B");
MakeLink(driver, "F", "C");
MakeLink(driver, "G", "C");
MakeLink(driver, "H", "G");
MakeLink(driver, "I", "G");
}
This method calls the MakeNode
method repeatedly to make the org chart's nodes. It then calls the MakeLink
method several times to make the org chart's relationships.
Notice that each call to MakeNode
and MakeLink
includes the transaction object that BuildOrgChart
received as a parameter.
The following code shows how the QueryOrgChart
method performs some queries on the finished org chart:
// Perform some queries on the org chart.
static void QueryOrgChart(IDriver driver)
{
List<string> result;
// Get F.
Console.WriteLine("F:");
result = ExecuteNodeQuery(driver,
"MATCH (n:OrgNode { ID:'F' }) " +
"return n");
Console.WriteLine($" {result[0]}");
// Who reports directly to B.
Console.WriteLine(" Reports directly to B:");
result = ExecuteNodeQuery(driver,
"MATCH " +
" (n:OrgNode)-[:REPORTS_TO]->(a:OrgNode { ID:'B' }) " +
"return n " +
"ORDER BY n.ID");
foreach (string s in result)
Console.WriteLine(" " + s);
// Chain of command for H.
Console.WriteLine(" Chain of command for H:");
result = FindPath(driver, "H", "A");
foreach (string s in result)
Console.WriteLine(" " + s);
// All reports for C.
Console.WriteLine(" All reports for C:");
result = ExecuteNodeQuery(driver,
"MATCH " +
" (n:OrgNode)-[:REPORTS_TO *]->(a:OrgNode { ID:'C' }) " +
"return n " +
"ORDER BY n.ID");
foreach (string s in result)
Console.WriteLine(" " + s);
}
This method first calls the ExecuteNodeQuery
method to execute the following query:
MATCH (n:OrgNode { ID:'F' }) return n
This simply finds the node with ID
equal to F
. The code prints it.
Next, the method looks for nodes that have the REPORTS_TO
relationship ending with node B. That returns all of the nodes that report directly to node B. The code loops through the results displaying them.
The method then uses the FindPath
method to find a path from node H to node A. Node A is at the top of the org chart, so this includes all the nodes in the chain of command from node H to the top.
The last query the method performs matches the following:
(n:OrgNode)-[:REPORTS_TO *]->(a:OrgNode { ID:'C' })
This finds nodes n
that are related via any number (*
) of REPORTS_TO
relationships to node C. That includes all the nodes that report directly or indirectly to node C. Graphically, those are the nodes that lie below node C in the org chart.
The previous methods make working with the org chart fairly simple. All we need to do now is get them started.
The following code shows the main program:
static void Main(string[] args)
{
// Replace the following with your database URI, username, and password.
string uri = "neo4j+s://386baeab.databases.neo4j.io";
string user = "neo4j";
string password = "InsertYourReallyLongAndSecurePasswordHere";
// Create the driver.
using (IDriver driver = GraphDatabase.Driver(uri,
AuthTokens.Basic(user, password)))
{
// Delete any previous nodes and links.
DeleteAllNodes(driver);
// Build the org chart.
BuildOrgChart(driver);
// Query the org chart.
QueryOrgChart(driver);
}
Console.Write(" Press Enter to quit");
Console.ReadLine();
}
This code first defines the uniform resource identifier (URI) where the database is located, the username, and the password. You can find these in the credential file that you downloaded when you created the database instance. (I hope you saved that file! If you didn't, then this might be a good time to delete the database instance and start over.)
Next, the code uses the URI, username, and password to create a graph database driver. Notice that the program creates the driver in a using
block so that it is automatically disposed of when the program is done with it.
The program then calls the DeleteAllNodes
, BuildOrgChart
, and QueryOrgChart
methods to do all the interesting work.
For its grand finale, the program displays a message and then waits for you to press Enter so that the output doesn't flash past and disappear before you can read it.
The following shows the program's output:
Deleting old data…
Building org chart…
Querying org chart…
F:
F: Mgr Pie and Food Gags
Reports directly to B:
D: Dir Puns and Knock-Knock Jokes
E: Dir Riddles
Chain of command for H:
H: Mgr Pratfalls
G: Dir Physical Humor
C: VP Shtick
A: President
All reports for C:
F: Mgr Pie and Food Gags
G: Dir Physical Humor
H: Mgr Pratfalls
I: Dir Sight Gags
Press Enter to quit
Figure 21.2 shows the same org chart in Figure 21.1, so you can look at it to see that the output is correct.
This chapter showed how you can use C# and a NoSQL graph database to build and explore an org chart. You can use similar techniques to work with other trees and, more generally, graphs that are not trees.
As you work with this example, you might notice that operations are relatively slow, particularly if you have a slow network connection. This is generally true of cloud applications. Network communications tend to be slower than local calculations.
The pattern that this example used was to:
Run
method to send Cypher commands to the database engine.The next two chapters show how to use a NoSQL document database in the cloud. Chapter 22 shows a Python example and Chapter 23 shows a similar example in C#. Before you move on to those chapters, however, use the following exercises to test your understanding of the material covered in this chapter. You can find the solutions to these exercises in Appendix A.
18.117.137.12