diff --git a/Neo4j.Driver.Extensions/RecordExtensions.cs b/Neo4j.Driver.Extensions/RecordExtensions.cs index 3a1a68e..8d9bc98 100644 --- a/Neo4j.Driver.Extensions/RecordExtensions.cs +++ b/Neo4j.Driver.Extensions/RecordExtensions.cs @@ -30,9 +30,17 @@ public static class RecordExtensions if (!string.IsNullOrWhiteSpace(identifier)) { - return record[identifier].TryAs(out var node) - ? node.ToObject() - : record[identifier].As>().ToObject(); + if (record[identifier].TryAs(out var node)) + { + return node.ToObject(); + } + + if(record[identifier].TryAs(out var relationship)) + { + return relationship.ToObject(); + } + + return record[identifier].As>().ToObject(); } var obj = new T(); diff --git a/Neo4j.Driver.Extensions/RelationshipExtensions.cs b/Neo4j.Driver.Extensions/RelationshipExtensions.cs new file mode 100644 index 0000000..eb24f37 --- /dev/null +++ b/Neo4j.Driver.Extensions/RelationshipExtensions.cs @@ -0,0 +1,133 @@ +namespace Neo4j.Driver.Extensions +{ + using System; + using System.Collections.Generic; + using System.Linq; + using EnsureThat; + using Neo4j.Driver; + + /// + /// A collection of extensions for the interface. + /// These should allow a user to deserialize things in an easier way. + /// + public static class RelationshipExtensions + { + /// + /// Gets a value from an instance. + /// + /// The to attempt to get the property as. + /// The instance to pull the property from. + /// The name of the property to get. + /// The converted or default + /// Thrown if is null. + /// Thrown if is null. + /// Thrown if is an empty string or whitespace. + /// + /// If any of the properties on the can't be cast to their + /// equivalents. + /// + public static T GetValue(this IRelationship relationship, string propertyName) + { + Ensure.That(relationship).IsNotNull(); + Ensure.That(propertyName).IsNotNullOrWhiteSpace(); + + return relationship.Properties.ContainsKey(propertyName) + ? relationship.Properties[propertyName].As() + : default; + } + + /// + /// Gets a value from an instance. Will throw a if the isn't on the . + /// + /// The to attempt to get the property as. + /// The instance to pull the property from. + /// The name of the property to get. + /// The converted or default + /// Thrown if is null. + /// Thrown if is null. + /// Thrown if is an empty string or whitespace. + /// + /// If any of the properties on the can't be cast to their + /// equivalents. + /// + /// Thrown if is not in the . + public static T GetValueStrict(this IRelationship relationship, string propertyName) + { + Ensure.That(relationship).IsNotNull(); + Ensure.That(propertyName).IsNotNullOrWhiteSpace(); + + if(relationship.Properties.ContainsKey(propertyName)) + return relationship.Properties[propertyName].As(); + + throw new KeyNotFoundException($"'{propertyName}' is not in the Relationship."); + } + + + /// + /// Gets a value from an instance, by executing + /// the method via reflection. + /// + /// This exists primarily to allow the method to be used to cast. + /// The instance to pull the property from. + /// The name of the property to get. + /// The to convert the property to. + /// If true this will throw if properties aren't found on the . + /// The converted value, as an . + /// If the is null. + /// If the is null. + /// If the is null. + /// + /// If any of the properties on the can't be cast to their + /// equivalents. + /// + 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}); + } + + /// + /// Attempts to cast the given instance into a complex type. + /// + /// The type to try to cast to. + /// The instance to cast from. + /// An instance of with it's properties set. + /// If the is null. + /// + /// If any of the properties on the can't be cast to their + /// equivalents. + /// + public static T ToObject(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; + } + } +} \ No newline at end of file diff --git a/Neo4j.Drivers.Extensions.Tests/RecordExtensionsTests.cs b/Neo4j.Drivers.Extensions.Tests/RecordExtensionsTests.cs index c4bfbb4..f70c402 100644 --- a/Neo4j.Drivers.Extensions.Tests/RecordExtensionsTests.cs +++ b/Neo4j.Drivers.Extensions.Tests/RecordExtensionsTests.cs @@ -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 @@ -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(); + mock.Setup(x => x[identifier]).Returns(new TestRelationship(new NodeReference(1), new { StringProperty = stringPropertyValue })); + + var foo = mock.Object.ToObject(identifier); + foo.StringProperty.Should().Be(stringPropertyValue); + } + [Fact] public void AssumesAllTheResponseIsAnObject_WhenIdentifierNotSupplied() { @@ -45,7 +64,7 @@ public void AssumesAllTheResponseIsAnObject_WhenIdentifierNotSupplied() var mock = new Mock(); mock.Setup(x => x.Keys).Returns(new List { "StringProperty", "stringPropertyWithAttribute" }); - mock.Setup(x => x.Values).Returns( new Dictionary { + mock.Setup(x => x.Values).Returns(new Dictionary { {"StringProperty", stringPropertyValue}, {"stringPropertyWithAttribute", stringPropertyWithAttributeValue}, }); @@ -56,6 +75,39 @@ public void AssumesAllTheResponseIsAnObject_WhenIdentifierNotSupplied() } } + public class TestRelationship : Relationship, + IRelationshipAllowingSourceNode, IRelationship + { + public TestRelationship(NodeReference targetNode, object data) : base(targetNode, data) + { + var dictionary = new Dictionary(); + + 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 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] @@ -158,8 +210,8 @@ public void ReturnsDefault_WhenIdentifierNotThere() mock.Setup(x => x.Keys).Returns(new List()); mock.Setup(x => x.Values).Returns(new Dictionary()); - var ex = Assert.Throws(() => mock.Object.GetValueStrict("not-there")); - ex.Should().NotBeNull(); + var ex = Assert.Throws(() => mock.Object.GetValueStrict("not-there")); + ex.Should().NotBeNull(); } [Fact] @@ -173,7 +225,7 @@ public void ReturnsCorrectValue() var mock = new Mock(); - mock.Setup(x => x.Keys).Returns(new List{stringIdentifier, intIdentifier}); + mock.Setup(x => x.Keys).Returns(new List { stringIdentifier, intIdentifier }); mock.Setup(x => x.Values).Returns(new Dictionary { {stringIdentifier, expectedStringValue},