Cameron Tinker
Cameron Tinker

Reputation: 9789

Attempting to build a Neo4j Repository

Before anyone asks, I do know that there is already a Neo4jClient repository, but it is dated and hasn't been updated for Neo4j 2.x and the new Neo4jClient code.

My goal is to achieve something like this:

var profiles = profileRepository.Get((Data.Model.Profile profile) => profile.Age > 20);

I first tried simply building the expression manually by passing the predicate to the Where call and executing Return on the CypherFluentQuery, but the predicate parameter didn't match what I had in the Return call:

return client.Cypher
       .Match("(entity:" + _entityTypeName + ")")
       .Where(predicate)
       .Return(entity => entity.As<TEntity>)
       .Results.FirstOrDefault();

This is ultimately why I decided that I need to build the return expression dynamically so that it would correctly name the parameters to pass to Neo4jClient and not come back with an undefined exception.

I've started researching Linq expression trees and lambda expressions to build a predicate based generic Neo4jClient repository in .NET 4.5 and Neo4j 2.x. I don't fully understand how to build an expression tree yet however. Here is my Neo4jRepository Get:

using Neo4jClient;
using Neo4jClient.Cypher;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;

namespace Data.Repository
{
    public class Neo4jRepository<TEntity> : IRepository<TEntity>
    {
        private List<TEntity> _entities;
        private string _entityTypeName;

        public Neo4jRepository()
        {
            _entities = new List<TEntity>();
            Type entityType = typeof(TEntity);
            _entityTypeName = entityType.Name;
        }

        public virtual IEnumerable<TEntity> Get(Expression<Func<TEntity, bool>> predicate)
        {
            var client = new GraphClient(
                              new Uri("http://localhost:7474/db/data"));
            client.Connect();

            ParameterExpression parameter =
                      Expression.Parameter(typeof(
                                 Expression<Func<ICypherResultItem, TEntity>>),
                                 predicate.Parameters[0].Name);

            var exp = Expression.Lambda<Func<ICypherResultItem, TEntity>>(
                parameters: parameter,
                body: Expression.Call(
                instance: Expression.Default(typeof(ICypherResultItem)),
                methodName: "As",
                typeArguments: new[] { typeof(TEntity) }
                )
            );

            return client.Cypher
                        .Match("(entity:" + _entityTypeName + ")")
                        .Where(predicate)
                        .Return(exp)
                        .Results;
        }
    }
}

Here's my IRepository:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;

namespace Data.Repository
{
    /// <summary>
    /// Generic repository that can be used with any data backend
    /// </summary>
    /// <typeparam name="TEntity">The type of the entity.</typeparam>
    interface IRepository<TEntity>
    {
        IEnumerable<TEntity> Get(Expression<Func<TEntity, bool>> predicate);
        void Add(TEntity item);
        void Update(Func<TEntity,bool> predicate, TEntity item);
        void Delete(TEntity item);
        void SaveChanges();
    }
}

When I try to do a get with a predicate, the compiler doesn't accept my lambda expression:

ParameterExpression of type 'System.Linq.Expressions.Expression1[System.Func2[Neo4jClient.Cypher.ICypherResultItem,Data.Model.Profile]]' cannot be used for delegate parameter of type 'Neo4jClient.Cypher.ICypherResultItem'

How do I build an expression with a dynamic parameter name based on my input predicate that will fit into the Return call like entity => entity.As<TEntity>?

Upvotes: 3

Views: 1323

Answers (1)

Cameron Tinker
Cameron Tinker

Reputation: 9789

After a little while of tinkering, I came up with this for a generic repository: https://github.com/pcmantinker/Neo4jRepository

Here is some sample usage:
BlogPost.cs

using System;
using System.Collections.Generic;
using System.Linq.Expressions;

namespace Neo4jRepository.Data.Model
{
    public class BlogPost : Neo4jEntity
    {
        public string Author { get; set; }
        public string Title { get; set; }
        public string Content { get; set; }
        public DateTimeOffset Created { get; set; }        

        public BlogPost()
        {
            Label = "BlogPost";
            Created = DateTimeOffset.UtcNow;
        }
    }
}

BlogPostRepository.cs

using Neo4jRepository.Data.Model;
using System.Threading.Tasks;

namespace Neo4jRepository.Repository
{
    public class BlogPostRepository : Neo4jRepository<BlogPost>
    {
        // any custom methods or overridden methods here

        /// <summary>
        /// Adds or updates a blog post.
        /// </summary>
        /// <param name="post">The post.</param>
        /// <returns></returns>
        public async Task AddOrUpdate(BlogPost post)
        {
            var found = await this.Single(p => p.Author == post.Author && p.Content == post.Content && p.Title == post.Title);
            if(found == null)
            {
                await Add(post);
            }
            else
            {
                await Update(p => p.Author == post.Author && p.Content == post.Content && p.Title == post.Title, post);
            }
        }
    }
}

Sample program:

BlogPostRepository _repo = new BlogPostRepository();
BlogPost post = new BlogPost() 
{
    Author = "John Smith",
    Title = "Hello Blog!",
    Content = "Test blog content"        
};
BlogPost post2 = new BlogPost() 
{
    Author = "Jane Smith",
    Title = "Hello Blog!",
    Content = "Test blog content"        
};
await _repo.AddOrUpdate(post);
await _repo.AddOrUpdate(post2);
IEnumerable<BlogPost> blogPosts = await _repo.All();
IEnumerable<BlogPost> janesPosts = await _repo.Where(b => b.Author == "Jane Smith");

Entities must inherit from Neo4jEntity to reference the label on the node for that entity. By making the repository generic, we can easily standup a repository for anything that inherits from Neo4jRepository with standard CRUD operations.

Upvotes: 3

Related Questions