0

Say I have the following (simplified):

public class Item
{
    public String Name { get; set; }
    public String Type { get; set; }
}

public class Armor : Item
{
    public int AC { get; set; }
    public Armor () { Type = "Armor"; }
}

public class Weapon : Item
{
     public int Damage { get; set; }
     public Armor () { Type = "Weapon"; }
}

public class Actor
{
    ...
}

public class HasItem : Relationship<ItemProps>, IRelationshipAllowingSourceNode<Actor>, IRelationshipAllowingTargetNode<Item>
{
    public readonly string TypeKey = "HasItem";

    public HasItem ( NodeReference targetItem, int count = 1 )
        : base(targetItem, new ItemProps { Count = count })
    {
    }

    public override string RelationshipTypeKey
    {
        get { return TypeKey; }
    }
}

With this setup I can easily create a heterogeneous list of Weapons, Armor, etc related to the Actor. But I can't seem to figure out how to get them out. I have this method (again simplified) to get a list of all the related items, but it gets them all out as Items. I can't figure out how to get them as their actual type. I can use the Type field to determine the type, but there doesn't seem to be anyway of dynamically building the return:

public IEnumerable<Item> Items
    {
        get
        {
            return
            GameNode
                .GraphClient
                .Cypher
                .Start(new { a = Node.ByIndexLookup("node_auto_index", "Name", Name) })
                .Match("(a)-[r:HasItem]-(i)")
                .Return<Item>("i") // Need something here to return Armor, Weapon, etc as needed based on the Type property
                .Results;
        }
    }

I found a bad workaround where I return the Type and NodeID and run the list through a switch statement that does a .Get with the NodeID and casts it to the right type. but this is inflexible and inefficient. I could run one query for each derived class and concatenate them together, but the thought of that makes my skin crawl.

This seems like it would be a common problem, but I couldn't find anything online. Any ideas?

1 Answers1

3

The problem is how the data is stored in Neo4J, and serialized back via Json.net.

Let's say I have a sword:

var sword = new Weapon{
    Name = "Sword 12.32.rc1",
    Type = "Sword"
    Damage = 12
};

If I serialize this to neo4j: graphClient.Create(sword); all is fine, internally we now have a Json representation which will look something like this:

{ "Name" : "Sword 12.32.rc1", "Type": "Sword", "Damage": "12"}

There is no information here that the computer can use to derive that this is in fact of type 'Sword', so if you bring back a collection of type Item it can only bring back the two properties Name and Type.

So, there are two solutions that I can think of, neither one of which is great, but both do get you with a one query solution. The first (most sucky) is to create a 'SuperItem' which has all the properties from the derived classes together, so:

public class SuperItem { Name, Type, Damage, AC } //ETC

But that is horrible, and kind of makes having a hierarchy pointless. The 2nd option, which whilst not great is better - is to use a Dictionary to get the data:

var query = GraphClient
    .Cypher
    .Start(new {n = actorRef})
    .Match("n-[:HasItem]->item")
    .Return(
    item => new
    {
        Item = item.CollectAs<Dictionary<string,string>>()
    });

var results = query.Results.ToList();

Which if you run:

foreach (var data in results2.SelectMany(item => item.Item, (item, node) => new {item, node}).SelectMany(@t => @t.node.Data))
    Console.WriteLine("Key: {0}, Value: {1}", data.Key, data.Value);

Would print out:

Key: Type, Value: Sword
Key: Damage, Value: 12
Key: Name, Value: 12.32.rc1

So, now we have a dictionary of the properties, we can create an extension class to parse it:

public static class DictionaryExtensions
{
    public static Item GetItem(this Dictionary<string, string> dictionary)
    {
        var type = dictionary.GetTypeOfItem().ToLowerInvariant();
        var json = dictionary.ToJson();
        switch (type)
        {
            case "sword":
                return GetItem<Weapon>(json);

            case "armor":
                return GetItem<Armor>(json);

            default:
                throw new ArgumentOutOfRangeException("dictionary", type, string.Format("Unknown type: {0}", type));
        }
    }

    private static string GetTypeOfItem(this Dictionary<string, string> dictionary)
    {
        if(!dictionary.ContainsKey("Type"))
            throw new ArgumentException("Not valid type!");

        return dictionary["Type"];
    }

    private static string ToJson(this Dictionary<string, string> dictionary)
    {
        var output = new StringBuilder("{");

        foreach (var property in dictionary.OrderBy(k => k.Key))
            output.AppendFormat("\"{0}\":\"{1}\",", property.Key, property.Value);

        output.Append("}");
        return output.ToString();
    }

    private static Item GetItem<TItem>(string json) where TItem: Item
    {
        return JsonConvert.DeserializeObject<TItem>(json);
    }
}

and use something like:

var items = new List<Item>();
foreach (var data in results)
    foreach (Node<Dictionary<string, string>> item in data.Item)
         items.Add(item.Data.GetItem());

Where items will be the types you're after.

I know this isn't great, but it does get you to one query.

Charlotte Skardon
  • 6,220
  • 2
  • 31
  • 42
  • that is just what I was looking for. I knew how the serialization works and why I was having the issue. I just didn't know how to get the "raw" data back so that I could roll my own objects. Thank you so much for your help! – Dread Pirate Peter Sep 09 '13 at 12:07