Recently I started working with Neo4j in C#. I am using the Neo4jClient and decided to write a wrapper for it using Generics. In this blog post I'll show how I did it. As always, your feedback is appreciated.
The first is the CRUD operations provider. CRUD stands for Create, Retrieve, Update, Delete in case you don't know. The CRUD provider is quite simple.
But before we jump into that, we need to make some concessions. We must have a common base type with a common property in order to find the node on successive calls. So I created a base class with a single property, “Id”, as an int. You can choose any property or properties you like but for simplicity I chose a small footprint. Here is my base class:
public abstract class Neo4jBase
{
public int Id { get; set; }
}
(I don’t know why the “I” is uppercase. I guess I got a little keyboard crazy.)
With that in place, we can build our CRUD data provider. Remember, you must be using the Neo4jClient library. Here is my generic provider:
using Neo4jClient;
public class Neo4jDataProvider<T> where T : Neo4jBase
{
IGraphClient _client = null;
public Neo4jDataProvider (IGraphClient client)
{
_client = client;
}
public T Create(T record)
{
if (_client != null)
{
var inputString = string.Format("({0}:{1}", typeof(T).Name.ToLower(), typeof(T).Name);
inputString += " { newRecord })";
_client.Cypher
.Create(inputString)
.WithParam("newRecord", record)
.ExecuteWithoutResults();
}
return record;
}
public T Retrieve(int id)
{
T result = null;
if (_client != null)
{
var inputString = string.Format("({0}:{1})", "record", typeof(T).Name);
var query = _client.Cypher
.Match(inputString)
.Where((T record) => record.Id == id)
.Return(record => record.As<T>());
result = query.Results.FirstOrDefault();
}
return result;
}
public T Update(T entry)
{
if (_client != null)
{
var inputString = string.Format("({0}:{1})", "record", typeof(T).Name);
_client.Cypher
.Match(inputString)
.Where((T record) => record.Id == entry.Id)
.Set("record = {updatedRecord}")
.WithParam("updatedRecord", entry)
.ExecuteWithoutResults();
}
return entry;
}
public void Delete(int id)
{
if (_client != null)
{
var inputString = string.Format("({0}:{1})", "record", typeof(T).Name);
_client.Cypher
.Match(inputString)
.Where((T record) => record.Id == id)
.Delete("record")
.ExecuteWithoutResults();
}
}
}
So what’s going here? First it is a generic class of type T where T is a Neo4jBase object. The reason is the Id property. That property allows us to find and deal with nodes individually in an easy and familiar fashion. Notice that the constructor takes an IGraphClient object. It is expected the IGraphClient is instantiated and connected to a database.
The next point to note is the use of the type of T as the label. For those of you new to graph databases, the “label” is the “type” of the node in the database. For example, labels may be “Person”, “Movie”, “Book”, etc. So in each method, the name of the type of T is used as the label. You will also see the word “record” scattered throughout the code. This is used as a variable name.
An interesting behavior is that the .WHERE methods seem to break if the variable name in the .MATCH methods is not the same name as the name of the variable in the lambda expression.
What about Relationships?
Remember that relationships are first-class citizens in graph databases, just like nodes. However, since relationships involve nodes of various types I decided to create a separate class for handling them. The class is still a generic class but instead uses two different types, TLEFT and TRIGHT.
A caveat: 1) The relationships data provider only handles unidirectional relationships (always left-to-right); Here is the relationship data provider:
using Neo4jClient;
public class Neo4jDataProviderRelationships<TLeft, TRight> where TLeft : Neo4jBase where TRight : Neo4jBase
{
IGraphClient _client = null;
public Neo4jDataProviderRelationships(IGraphClient client)
{
_client = client;
}
public void Associate(TLeft left, string relationshipName, TRight right)
{
if (_client != null)
{
var inputStringLeft = string.Format("({0}:{1})", "tleft", typeof(TLeft).Name);
var inputStringRight = string.Format("({0}:{1})", "tright", typeof(TRight).Name);
_client.Cypher
.Match(inputStringLeft, inputStringRight)
.Where((TLeft tleft) => tleft.Id == left.Id)
.AndWhere((TRight tright) => tright.Id == right.Id)
.Create("tleft-[:" + relationshipName + "]->tright")
.ExecuteWithoutResults();
}
}
public void Dissociate(TLeft left, string relationshipName, TRight right)
{
if (_client != null)
{
var inputStringLeft = string.Format("({0}:{1})", "tleft", typeof(TLeft).Name);
var inputStringRight = string.Format("({0}:{1})", "tright", typeof(TRight).Name);
_client.Cypher
.Match(inputStringLeft + "-[:" + relationshipName + "]->" + inputStringRight)
.Where((TLeft tleft) => tleft.Id == left.Id)
.AndWhere((TRight tright) => tright.Id == right.Id)
.Delete(relationshipName)
.ExecuteWithoutResults();
}
}
}
Notice that like the CRUD data provider, an IGraphClient is expected by the constructor. There are two methods for associating and dissociating nodes.
Usage
So how are these classes used? Well, suppose you have a class, Person, and a class, Movie as follows (borrowed from the demo database that ships with Neo4j):
public class Movie : Neo4jBase
{
public string title { get; set; }
public string released { get; set; }
public string tagline { get; set; }
}
public class Person : Neo4jBase
{
public string name { get; set; }
public string born { get; set; }
}
Now suppose you want to create a Person and a Movie and relate them. Specifically, let’s create William Shatner, and Airplane II, and relate them. First, create the nodes:
Neo4jDataProvider<Person> personDataProvider = new Neo4jDataProvider<Person>(client);
Person person = new Person { Id = 1, born = "1966", name = "William Shatner" };
personDataProvider.Create(person);
Neo4jDataProvider<Movie> movieDataProvider = new Neo4jDataProvider<Movie>(client);
var movie = new Movie { Id = 1, released = "1982", tagline = "The Sequel", title = "Airplane II" };
movieDataProvider.Create(movie);
That will create the two nodes. Let’s relate them:
Neo4jDataProviderRelationships<Person, Movie> relDataProvider = new Neo4jDataProviderRelationships<Person, Movie>(client);
relDataProvider.Associate(person, "ACTED_IN", movie);
And that’s it!
What’s the catch?
There are a number of catches including:
- You must manage the “Id” values yourself.
- You can’t easily retrieve a node based on any other property.
- You can’t retrieve relationships. Even though they are fist class citizens, I only created CRUD operations wrappers.
- The “Create” method doesn’t check for duplicates. The “Update” method allows changing the “Id” field.
I’m sure there are more catches. As an apology, I am just getting started. I hope to extend these classes with more robust operations. Your feedback is appreciated. If someone has already done this (or something similar) I’d love to know about it.