Skip to content

Commit

Permalink
Records can parse relations and callers can access data of the relati…
Browse files Browse the repository at this point in the history
…onship
  • Loading branch information
CasperAtVicompany committed Dec 2, 2021
1 parent a714324 commit ac495d2
Show file tree
Hide file tree
Showing 3 changed files with 201 additions and 8 deletions.
14 changes: 11 additions & 3 deletions Neo4j.Driver.Extensions/RecordExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,17 @@ public static class RecordExtensions

if (!string.IsNullOrWhiteSpace(identifier))
{
return record[identifier].TryAs<INode>(out var node)
? node.ToObject<T>()
: record[identifier].As<IDictionary<string, object>>().ToObject<T>();
if (record[identifier].TryAs<INode>(out var node))
{
return node.ToObject<T>();
}

if(record[identifier].TryAs<IRelationship>(out var relationship))
{
return relationship.ToObject<T>();
}

return record[identifier].As<IDictionary<string, object>>().ToObject<T>();
}

var obj = new T();
Expand Down
133 changes: 133 additions & 0 deletions Neo4j.Driver.Extensions/RelationshipExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
namespace Neo4j.Driver.Extensions
{
using System;
using System.Collections.Generic;
using System.Linq;
using EnsureThat;
using Neo4j.Driver;

/// <summary>
/// A collection of extensions for the <see cref="IRelationship"/> interface.
/// These should allow a user to deserialize things in an easier way.
/// </summary>
public static class RelationshipExtensions
{
/// <summary>
/// Gets a value from an <see cref="IRelationship" /> instance.
/// </summary>
/// <typeparam name="T">The <see cref="Type" /> to attempt to get the property as.</typeparam>
/// <param name="relationship">The <see cref="IRelationship" /> instance to pull the property from.</param>
/// <param name="propertyName">The name of the property to get.</param>
/// <returns>The converted <typeparamref name="T" /> or <c>default</c></returns>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="relationship"/> is null.</exception>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="propertyName"/> is null.</exception>
/// <exception cref="ArgumentException">Thrown if <paramref name="propertyName"/> is an empty string or whitespace.</exception>
/// <exception cref="FormatException">
/// If any of the properties on the <paramref name="relationship" /> can't be cast to their
/// <typeparamref name="T" /> equivalents.
/// </exception>
public static T GetValue<T>(this IRelationship relationship, string propertyName)
{
Ensure.That(relationship).IsNotNull();
Ensure.That(propertyName).IsNotNullOrWhiteSpace();

return relationship.Properties.ContainsKey(propertyName)
? relationship.Properties[propertyName].As<T>()
: default;
}

/// <summary>
/// Gets a value from an <see cref="IRelationship" /> instance. Will throw a <see cref="KeyNotFoundException"/> if the <paramref name="propertyName"/> isn't on the <paramref name="relationship"/>.
/// </summary>
/// <typeparam name="T">The <see cref="Type" /> to attempt to get the property as.</typeparam>
/// <param name="relationship">The <see cref="IRelationship" /> instance to pull the property from.</param>
/// <param name="propertyName">The name of the property to get.</param>
/// <returns>The converted <typeparamref name="T" /> or <c>default</c></returns>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="relationship"/> is null.</exception>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="propertyName"/> is null.</exception>
/// <exception cref="ArgumentException">Thrown if <paramref name="propertyName"/> is an empty string or whitespace.</exception>
/// <exception cref="FormatException">
/// If any of the properties on the <paramref name="relationship" /> can't be cast to their
/// <typeparamref name="T" /> equivalents.
/// </exception>
/// <exception cref="KeyNotFoundException">Thrown if <paramref name="propertyName"/> is not in the <paramref name="relationship"/>.</exception>
public static T GetValueStrict<T>(this IRelationship relationship, string propertyName)
{
Ensure.That(relationship).IsNotNull();
Ensure.That(propertyName).IsNotNullOrWhiteSpace();

if(relationship.Properties.ContainsKey(propertyName))
return relationship.Properties[propertyName].As<T>();

throw new KeyNotFoundException($"'{propertyName}' is not in the Relationship.");
}


/// <summary>
/// Gets a value from an <see cref="IRelationship" /> instance, by executing
/// the <see cref="GetValue{T}" /> method via reflection.
/// </summary>
/// <remarks>This exists primarily to allow the <see cref="ValueExtensions.As{T}(object)" /> method to be used to cast.</remarks>
/// <param name="relationship">The <see cref="IRelationship" /> instance to pull the property from.</param>
/// <param name="propertyName">The name of the property to get.</param>
/// <param name="propertyType">The <see cref="Type" /> to convert the property to.</param>
/// <param name="strict">If <c>true</c> this will throw <see cref="KeyNotFoundException"/> if properties aren't found on the <see cref="relationship"/>.</param>
/// <returns>The converted value, as an <see cref="object" />.</returns>
/// <exception cref="ArgumentNullException">If the <paramref name="relationship" /> is null.</exception>
/// <exception cref="ArgumentNullException">If the <paramref name="propertyName" /> is null.</exception>
/// <exception cref="ArgumentNullException">If the <paramref name="propertyType" /> is null.</exception>
/// <exception cref="InvalidCastException">
/// If any of the properties on the <paramref name="relationship" /> can't be cast to their
/// <typeparamref name="T" /> equivalents.
/// </exception>
private static object GetValue(this IRelationship relationship, string propertyName, Type propertyType, bool strict = false)
{
Ensure.That(relationship).IsNotNull();
Ensure.That(propertyName).IsNotNull();
Ensure.That(propertyType).IsNotNull();

var method = strict
? typeof(RelationshipExtensions).GetMethod(nameof(GetValueStrict))
: typeof(RelationshipExtensions).GetMethod(nameof(GetValue));

var generic = method?.MakeGenericMethod(propertyType);

return generic?.Invoke(relationship, new object[] {relationship, propertyName});
}

/// <summary>
/// Attempts to cast the given <see cref="IRelationship" /> instance into a complex type.
/// </summary>
/// <typeparam name="T">The type to try to cast to.</typeparam>
/// <param name="relationship">The <see cref="IRelationship" /> instance to cast from.</param>
/// <returns>An instance of <typeparamref name="T" /> with it's properties set.</returns>
/// <exception cref="ArgumentNullException">If the <paramref name="relationship" /> is null.</exception>
/// <exception cref="InvalidCastException">
/// If any of the properties on the <paramref name="relationship" /> can't be cast to their
/// <typeparamref name="T" /> equivalents.
/// </exception>
public static T ToObject<T>(this IRelationship relationship) where T : new()
{
Ensure.That(relationship).IsNotNull();

var properties = typeof(T).GetProperties().Where(p => p.CanWrite);
var obj = new T();
foreach (var property in properties)
{
var propertyName = property.Name;
var neo4jProperty = property.GetNeo4jPropertyAttribute();
if (neo4jProperty != null)
{
if (neo4jProperty.Ignore)
continue;

propertyName = neo4jProperty.Name ?? propertyName;
}

property.SetValue(obj, relationship.GetValue(propertyName, property.PropertyType));
}

return obj;
}
}
}
62 changes: 57 additions & 5 deletions Neo4j.Drivers.Extensions.Tests/RecordExtensionsTests.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
namespace Neo4j.Drivers.Extensions.Tests
using System.ComponentModel;

namespace Neo4j.Drivers.Extensions.Tests
{
using System;
using System.ComponentModel;
using System.Collections.Generic;
using FluentAssertions;
using Moq;
using Neo4j.Driver;
using Neo4j.Driver.Extensions;

using Neo4jClient;

using Xunit;

public class RecordExtensionsTests
Expand Down Expand Up @@ -37,6 +43,19 @@ public void TreatsRecordAsDictionary_WhenIdentifierSupplied()
foo.StringPropertyWithAttribute.Should().Be(stringPropertyWithAttributeValue);
}

[Fact]
public void TreatsRecordAsRelationShip_WhenIdentifierSupplied()
{
const string identifier = "foo";
const string stringPropertyValue = "baa";

var mock = new Mock<IRecord>();
mock.Setup(x => x[identifier]).Returns(new TestRelationship(new NodeReference(1), new { StringProperty = stringPropertyValue }));

var foo = mock.Object.ToObject<Foo>(identifier);
foo.StringProperty.Should().Be(stringPropertyValue);
}

[Fact]
public void AssumesAllTheResponseIsAnObject_WhenIdentifierNotSupplied()
{
Expand All @@ -45,7 +64,7 @@ public void AssumesAllTheResponseIsAnObject_WhenIdentifierNotSupplied()

var mock = new Mock<IRecord>();
mock.Setup(x => x.Keys).Returns(new List<string> { "StringProperty", "stringPropertyWithAttribute" });
mock.Setup(x => x.Values).Returns( new Dictionary<string, object> {
mock.Setup(x => x.Values).Returns(new Dictionary<string, object> {
{"StringProperty", stringPropertyValue},
{"stringPropertyWithAttribute", stringPropertyWithAttributeValue},
});
Expand All @@ -56,6 +75,39 @@ public void AssumesAllTheResponseIsAnObject_WhenIdentifierNotSupplied()
}
}

public class TestRelationship : Relationship,
IRelationshipAllowingSourceNode<Foo>, IRelationship
{
public TestRelationship(NodeReference targetNode, object data) : base(targetNode, data)
{
var dictionary = new Dictionary<string, object>();

foreach (var propDesc in data.GetType().GetProperties())
{
dictionary.Add(propDesc.Name, propDesc.GetValue(data));
}
this.Properties = dictionary;
}

public override string RelationshipTypeKey
{
get { throw new NotImplementedException(); }
}

public object this[string key] => throw new NotImplementedException();

public IReadOnlyDictionary<string, object> Properties { get; }
public long Id { get; }
public bool Equals(IRelationship other)
{
throw new NotImplementedException();
}

public string Type { get; }
public long StartNodeId { get; }
public long EndNodeId { get; }
}

public class GetValueT
{
[Fact]
Expand Down Expand Up @@ -158,8 +210,8 @@ public void ReturnsDefault_WhenIdentifierNotThere()
mock.Setup(x => x.Keys).Returns(new List<string>());
mock.Setup(x => x.Values).Returns(new Dictionary<string, object>());

var ex = Assert.Throws<KeyNotFoundException>(() => mock.Object.GetValueStrict<int>("not-there"));
ex.Should().NotBeNull();
var ex = Assert.Throws<KeyNotFoundException>(() => mock.Object.GetValueStrict<int>("not-there"));
ex.Should().NotBeNull();
}

[Fact]
Expand All @@ -173,7 +225,7 @@ public void ReturnsCorrectValue()
var mock = new Mock<IRecord>();


mock.Setup(x => x.Keys).Returns(new List<string>{stringIdentifier, intIdentifier});
mock.Setup(x => x.Keys).Returns(new List<string> { stringIdentifier, intIdentifier });
mock.Setup(x => x.Values).Returns(new Dictionary<string, object>
{
{stringIdentifier, expectedStringValue},
Expand Down

0 comments on commit ac495d2

Please sign in to comment.