Статьи

Познакомьтесь с Neo4jClient

Neo4jClient — это клиент .NET для Neo4j Rest Server, созданный моим коллегой Тэтэмом Одди. В настоящее время у нас есть Neo4jClient, который является последней сборкой исходного кода. Это можно найти в NuGet с именем пакета Neo4jClient. Neo4jClient — это клиент .NET для Neo4j Rest Server.


NuGetPackage:

Исходный код на:

Он использует RestSharp и Newtonsoft.json для сериализации и десериализации Rest и Json соответственно.

Клиент также поддерживает выполнение запросов Gremlin следующими способами.

  • Отправить сырые операторы gremlin, которые возвращают скалярные результаты
  • Отправить необработанные операторы gremlin, которые возвращают перечисляемую коллекцию узлов
  • Введите безопасные лямбда-выражения

Необработанная версия предоставляется для обеспечения гибкости, если вы обнаружите, что некоторые лямбда-выражения не поддерживаются, со временем будет добавлено больше выражений. Мы будем расширять клиента с течением времени для некоторых других выражений, поскольку нам понадобятся новые выражения.

Давайте посмотрим на некоторые образцы.

Сначала вам нужно установить пакет, поэтому в диспетчере консоли пакета введите:

установочный пакет neo4jclient

После того, как это будет установлено, вы, конечно, сможете загрузить IGraphClient в точку входа вашего приложения, вручную или с помощью IoC. Концепция GraphClient заключается в предоставлении базовых операций CRUD.

The other powerful aspect is that the IGremlinQuery interface provide cool extensions into Gremlin so that a series of extensions methods will support out and in vertices and edges querying.

Below is a diagram illustrating the core concept.Basically we have a GraphClient, GremlinQuery and NodeReference. Notice that you can also query directly off a NodeReference. A NodeReference will represent a reference to a Vertice in the database. A Node will store the actual data, which is cast to a specific type with generics.

образ

Entry Point

Here is sample code in loading the GraphClient using an IoC. It is this easy to get it started.

builder.Register<IGraphClient>(context =>
            {
                var resolver = context.Resolve<IRoleEndpointResolver>();
                var baseUri = resolver.GetNeo4JBaseUri();
                var graphClient = new GraphClient(baseUri);
                graphClient.Connect();
                return graphClient;
            })
            .SingleInstance();

Type Safe Lambda Expressions

Lets look at the cool features we can do. Below is a sample query we can run.

public Node<User> GetUser(User identifier)
        {
            if (identifier.IsSysAdmin)
            {
                return graphClient
                    .RootNode
                    .In<User>(Administers.TypeKey, u=> u.Username == identifier.Username)
                    .SingleOrDefault();
            }

            return graphClient
                .RootNode
                .Out<CustomerSite>(Hosts.TypeKey, a => a.Key == identifier.CustomerSiteKey)
                .In<User>(UserBelongsTo.TypeKey, u => u.Username == identifier.Username)
                .SingleOrDefault();
        }

You can even then run queries off a NodeReference, lets look at an example.

public int CreateUser(User user, NodeReference<Company> companyReference)
{
    return companyReference
                .InE(UserBelongsTo.TypeKey)
                .OutE<User>(u=>u.Username == user.Username)
}

You have the flexibility.

Creating Nodes and Relationships

You notice that in the above, we had a TypeKey representing the relationship, this is important, you can enforce very strict rules on your nodes, we can define a class that represents a relationship and enforce which source and target nodes or data models it is allowed to have e.g.

public class UserBelongsTo :
        Relationship,
        IRelationshipAllowingSourceNode<User>,
        IRelationshipAllowingTargetNode<CustomerSite>
    {
        public UserBelongsTo(NodeReference targetNode)
            : base(targetNode)
        {
        }

        public const string TypeKey = "USER_BELONGS_TO";
        public override string RelationshipTypeKey
        {
            get { return TypeKey; }
        }
    }

This means, that you get compile time safety, if you try and create a node in the database with a relationship. Lets look at a Create statement.

public NodeReference CreateUser(User user, NodeReference<CustomerSite> customerSite)
        {
            user.Username = user.Username.ToLowerInvariant();

            var node = graphClient.Create(
                user,
                new UserBelongsTo(customerSite));

            return node;
        }

Above, if you tried to swap the create, it would not work e.g.

public NodeReference CreateUser(User user, NodeReference<CustomerSite> customerSite)
        {
            user.Username = user.Username.ToLowerInvariant();
            var node = graphClient.Create(
                customerSite,
                new UserBelongsTo(user));

            return node;
        }

Updates

You can also update nodes, this is done by passing in a NodeReference and a delegate e.g.

public void UpdateUser(User user, Action<User> updateCallback)
{
            graphClient.Update(userNode.Reference, u =>
                {
                    updateCallback(u);
                });
}

Notice, you also get type safety here as well. The reference to the delegate/method with then get executed when neo4jClient persists the data.

Here is the sample updateCallback call to the above method. Notice I am in fact using the MVC updateModel facility to do the mappings for me, any how, you can update the object using your logic of course, there is no restrictions here. Here “this” refers to the MVC Controller class. It is just a nice way to get MVC to compare the user with the user from the DB and then merge the changes, no need to write logic to merge changes if using in the context of MVC, since it has a nice UpdateModel method that we can use. Othewise you would be using AutoMapper or something even nicer like ValueInjecter(http://valueinjecter.codeplex.com/).

userService.UpdateUser(user, u => this.UpdateModel(u,
                UpdateModel,
                tu => tu.GivenName,
                tu => tu.FamilyName,
                tu => tu.Username,
                tu => tu.EmailAddress,
                tu => tu.BusinessPhone,
                tu => tu.MobilePhone
            ));

The update above looks a bit tricky at first as we see a method pointer to a method pointer i.e. two action calls, in fact there are 4, one from the graph client, one from the service, one from the extended version of UpdateModel and then the updateModel itself.

The reason why I have a custom extension method for UpdateModel, is so we can Explicitly set which columns to update. Remember UpdateModel takes a callback and a list of properties to explicitly update, you can of course just call the default version if ALL fields need to be updated.

userService.UpdateUser(user, UpdateModel);

Below is the code for the extended UpdateModel.

public static class ControllerExtensions
    {
        public static void UpdateModel<TModel>(this Controller controller, TModel instanceToUpdate, Action<TModel, string[]> updateModelCallback, params Expression<Func<TModel, object>>[] includeProperties)
        {
            var propertyNames = GetPropertyNames(includeProperties);
            updateModelCallback(instanceToUpdate, propertyNames.ToArray());
        }

        public static IEnumerable<string> GetPropertyNames<T>(params Expression<Func<T, object>>[] action)
        {
            return action
                .Select(property =>
                {
                    var body = property.Body;

                    var unaryExpression = body as UnaryExpression;
                    if (unaryExpression != null) body = unaryExpression.Operand;

                    return (MemberExpression) body;
                })
                .Select(expression => expression.Member.Name);
        }
    }

The above extension method will now allow you to call the UpdateModel with type safety on the model fields to explicitly update. As i mentioned, if you need a simpler update to default to all fields then this call will work:

Deletes

You can also delete data. Notice the pattern here, get a node reference then run an operation.

graphClient.Delete(node.Reference,DeleteMode.NodeAndRelationships);

Relationships

You can also create relationships between existing nodes.

graphClient.CreateRelationship(customerNodeReference, new Speaks(languageNode.Reference));

Scalar Query Result – Raw

You might find that a complex lambda is not support, in which case you can execute a raw statement directly an still get type safety e.g.

var count = g.v(0).out('IsCustomer').Count()

Rest under the hood

The Client is smart enough to query the rest endpoint on the server and utilize the correct rest points. So the baseUri would be something like http://locahost:7474/db/data

The graphclient will do a Http get request with application/json to the above endpoint and will retrieve the following response.

{
  "index" : "http://localhost:7474/db/data/index",
  "node" : "http://localhost:7474/db/data/node",
  "reference_node" : "http://localhost:7474/db/data/node/0"
}

The above endpoints is the fundamental way to run rest queries, of course the graphclient does all the work for you, however it is always nice to know how it works.

Summary

So this is my little primer on the project we working with and it is fantastic working with someone like Tatham Oddie who built this brilliant tool for us to use.

We will be extending this tool as we go along and build new functionality as we need it. Not all Lambda expressions are supported yet and it is limited in tis regard but it is easy to extend going forward.