Skip to content

Writing queries

Introduction

The code snippets in this document are designed to complement any . NET template created using the steps outlined in the Getting started section. Throughout this guide, we leverage the domain model introduced by Kelvin Lawrence in PRACTICAL GREMLIN: An Apache TinkerPop Tutorial.

This domain model represents a fictional airline network as a graph, where airports serve as vertices and flights between them as edges. Using this intuitive model as our foundation, we'll explore a variety of Gremlinq queries to extract meaningful insights from the airline network graph. The entities are defined as follows:

C#
public sealed class Airport : Vertex
{
    public string? Code { get; set; }
    public string? ICAO { get; set; }
    public string? City { get; set; }
    public string? Region { get; set; }
    public string? Country { get; set; }
    public string? Description { get; set; }

    public int Runways { get; set; }
    public int Elevation { get; set; }
    public double Latitude { get; set; }
    public double Longitude { get; set; }
    public int LongestRunway { get; set; }
}

public sealed class Route : Edge
{
    public long Distance { get; set; }
}

Create the AirRoutes dataset

The AirRoutes dataset is initialized by calling CreateAirRoutesSmall. This method is idempotent - it checks for existing nodes and edges before creating new ones, so calling it multiple times is safe. To improve performance after the initial setup, you can comment out this line once the data has been loaded into your database.

C#
await _g
    .CreateAirRoutesSmall();

Simple queries

Retrieve all the airports
C#
var airports = await _g
    .V<Airport>()
    .ToArrayAsync();
How many airports are in the database?
C#
var airportCount = await _g
    .V<Airport>()
    .Count()
    .FirstAsync();
Which airport have their name starting with the letter 'S'?
C#
var airportCodesStartingWithLetterS = await _g
    .V<Airport>()
    .Where(airport => airport.Code!.StartsWith("S"))
    .ToArrayAsync();

Walk the graph

Find all destination airports from SEA
C#
var destinationsFromSeattle = await _g
    .V<Airport>()
    .Where(airport => airport.Code == "SEA")
    .Out<Route>()
    .OfType<Airport>()
    .ToArrayAsync();
Find all airports connected to SEA within 1500 miles
C#
var within1500Miles = await _g
    .V<Airport>()
    .Where(airport => airport.Code == "SEA")
    .OutE<Route>()
    .Where(route => route.Distance <= 1500)
    .InV<Airport>()
    .ToArrayAsync();
Find all airports that can reach SEA
C#
var routesIntoSEA = await _g
    .V<Airport>()
    .Where(airport => airport.Code == "SEA")
    .In<Route>()
    .OfType<Airport>()
    .ToArrayAsync();
Find all airports that are reachable from SEA by taking two flights
C#
var twoFlightsFromSeattle = await _g
    .V<Airport>()
    .Where(airport => airport.Code == "SEA")
    .Out<Route>()
    .Out<Route>()
    .OfType<Airport>()
    .Dedup()
    .ToArrayAsync();

Using sub-queries

Find all airports that are reachable from SEA by taking one or two flights
C#
var withinOneOrTwoFlights = await _g
    .V<Airport>()
    .Where(airport => airport.Code == "SEA")
    .Union(
        __ => __
            .Out<Route>(),
        __ => __
            .Out<Route>()
            .Out<Route>())
    .OfType<Airport>()
    .Dedup()
    .ToArrayAsync();

Simpler:

C#
await _g
    .V<Airport>()
    .Where(airport => airport.Code == "SEA")
    .Out<Route>()
    .Union(
        __ => __,
        __ => __
            .Out<Route>())
    .OfType<Airport>()
    .Dedup()
    .ToArrayAsync();
Find all destinations from SEA that have a connection to Atlanta
C#
var destinationsWithRoutesToAtlanta = await _g
    .V<Airport>()
    .Where(airport => airport.Code == "SEA")
    .Out<Route>()
    .OfType<Airport>()
    .Where(__ => __
        .Out<Route>()
        .OfType<Airport>()
        .Where(airport => airport.Code == "ATL"))
    .ToArrayAsync();

Orderings

This section marks the first introduction of a special of a sub-domain-specific language (sub-DSL), enabling developers to ensure that only sensible methods are within scope. This sub-DSL streamlines the programming process by providing a focused and tailored set of methods relevant to the specific context by providing a builder instance.

Order all airports lexically by their code
C#
var orderedByCode = await _g
    .V<Airport>()
    .Order(o => o
        .By(airport => airport.Code))
    .ToArrayAsync();
Order all airports by their longest route
C#
var orderedByLongestRoute = await _g
    .V<Airport>()
    .Order(o => o
        .ByDescending(__ => __
            .OutE<Route>()
            .Order(o => o
                .ByDescending(route => route.Distance))
            .Values(route => route.Distance)))
    .ToArrayAsync();
Order all airports by their longest route, then lexically by their code
C#
var orderedByLongestRouteThenCode = await _g
    .V<Airport>()
    .Order(o => o
        .ByDescending(__ => __
            .OutE<Route>()
            .Order(o => o
                .ByDescending(route => route.Distance))
            .Values(route => route.Distance))
        .By(airport => airport.Code))
    .ToArrayAsync();

Limiting results

Get the five airports with the longest routes
C#
var fiveAirportsOrderedByLongestRoute = await _g
    .V<Airport>()
    .Order(o => o
        .ByDescending(__ => __
            .OutE<Route>()
            .Order(o => o
                .ByDescending(route => route.Distance))
            .Values(route => route.Distance)))
    .Limit(5)
    .ToArrayAsync();
Using Range instead
C#
var fiveAirportsWithRange = await _g
    .V<Airport>()
    .Order(orderBuilder => orderBuilder
        .ByDescending(__ => __
            .OutE<Route>()
            .Order(o => o
                .ByDescending(x => x.Distance))
            .Values(x => x.Distance)))
    .Range(0, 5)
    .ToArrayAsync();
Skip the first five results
C#
var fiveAirportsWithRange = await _g
    .V<Airport>()
    .Order(orderBuilder => orderBuilder
        .ByDescending(__ => __
            .OutE<Route>()
            .Order(o => o
                .ByDescending(x => x.Distance))
            .Values(x => x.Distance)))
    .Range(0, 5)
    .ToArrayAsync();
Take only the latter five
C#
var tailFiveResults = await _g
    .V<Airport>()
    .Order(orderBuilder => orderBuilder
        .ByDescending(__ => __
            .OutE<Route>()
            .Order(o => o
                .ByDescending(x => x.Distance))
            .Values(x => x.Distance)))
    .Tail(5)
    .ToArrayAsync();

Step labels

Avoid returning to the departure airport right away
C#
var withinTwoFlightsWithNoReturn = await _g
    .V<Airport>()
    .Where(departure => departure.Code == "SEA")
    .As((__, sea) => __
        .Out<Route>()
        .Out<Route>()
        .OfType<Airport>()
        .Where(destination => destination != sea.Value))
    .Dedup()
    .ToArrayAsync();

Projections

Project to value tuples
C#
var projectedTuple = await _g
    .V<Airport>()
    .Project(p => p
        .ToTuple()
        .By(x => x.Description!)
        .By(x => x.Code!)
        .By(__ => __.Out<Route>().Count()))
    .FirstAsync();
Project to value tuples with sub-query
C#
var projectedTupleWithSubQuery = await _g
    .V<Airport>()
    .Project(p => p
        .ToTuple()
        .By(x => x.Code!)
        .By(__ => __
            .Out<Route>()
            .OfType<Airport>()
            .Values(x => x.Code!)
            .Fold()))
    .ToArrayAsync();
Project to dynamics
C#
var projectToDynamic = await _g
   .V<Airport>()
   .Project(p => p
       .ToDynamic()
       .By(x => x.Code!)
       .By(x => x.Description!))
   .ToArrayAsync();
Project to dynamics with explicit names
C#
var projectToDynamicExplicit = await _g
   .V<Airport>()
   .Project(p => p
       .ToDynamic()
       .By(x => x.Code!)
       .By(x => x.Description!))
   .ToArrayAsync();
Project to custom data structure instances
C#
var projectToRecords = await _g
    .V<Airport>()
    .Project(p => p
        .To<DepartureAndDestinationRecord>()
        .By(
            x => x.DepartureCode,
            x => x.Code)
        .By(
            x => x.DestinationCodes,
            __ => __
                .Out<Route>()
                .OfType<Airport>()
                .Values(x => x.Code!)
                .Fold()))
   .ToArrayAsync();

Aggregates

Fold
C#
var fold = await _g
    .V<Airport>()
    .Map(__ => __
        .Out<Route>()
        .OfType<Airport>()
        .Values(x => x.Code!)
        .Fold())
    .ToArrayAsync();
Sum
C#
var sumOfRoutes = await _g
    .V<Airport>()
    .Where(x => x.Code == "SEA")
    .OutE<Route>()
    .Values(x => x.Distance)
    .Sum()
    .FirstAsync();
Max
C#
var maximumDistanceFromSEA = await _g
    .V<Airport>()
    .Where(x => x.Code == "SEA")
    .Map(__ => __
        .OutE<Route>()
        .Values(x => x.Distance)
        .Max())
    .FirstAsync();

Groupings

Building upon the previously introduced concept of sub-DSL, building groups is another case where such a sub-DSL is employed:

Group airports by number of routes
C#
var groupByNumberOfRoutes = await _g
    .V<Airport>()
    .Group(g => g
        .ByKey(__ => __
            .OutE<Route>()
            .Count()))
    .ToArrayAsync();
Project to specific group values

The previous example illustrated a grouping by numbers without explicitly specifying a value, resulting in the projection of values to the airports themselves. However, it's worth noting that we can also project values specifically to the airport codes:

C#
var groupCodesByNumberOfRoutes = await _g
    .V<Airport>()
    .Group(g => g
        .ByKey(__ => __
            .OutE<Route>()
            .Count())
        .ByValue(__ => __
            .Values(x => x.Code!)))
    .ToArrayAsync();

Loops

Again, there is a specific DSL for loops that, through a builder, lets the user chain reasonable combinations of the Emit, Repeat, Until and Times operators:

Where can we go with three flights?
C#
var threeFlights = await _g
    .V<Airport>()
    .Map(__ => __
        .Loop(l => l
            .Repeat(__ => __
                .Out<Route>()
                .OfType<Airport>())
            .Times(3))
        .Dedup()
        .Values(x => x.Code!)
        .Fold())
    .ToArrayAsync();
Unroll all the paths from SEA until we reach Atlanta or end up in a cycle
C#
var repeatEmitUntilAtlanta = await _g
    .V<Airport>()
    .Where(x => x.Code == "SEA")
    .Loop(l => l
        .Repeat(__ => __
            .Out<Route>()
            .OfType<Airport>())
        .Emit()
        .Until(__ => __
            .Where(a => a.Code == "ATL")))
    .Dedup()
    .Limit(10)
    .Values(x => x.Code!)
    .ToArrayAsync();

Trees

A tree in the TinkerPop sense is essentially just a wrapper for a Dictionary<TRoot, TTree> where TRoot can be anything (scalars, entities) and TTree is, again, a Tree. So a tree in this sense can be seen as having many root nodes where each of the root nodes maps to a subtree.

Gremlinq defines the non-generic ITree-marker interface and two derived classes:

  • Tree<TNode> is a tree where each node in the tree is of type TNode
  • Tree<TRoot, TTree> is a tree where the roots are of type TRoot and the subtrees are of type TTree.

There are currently 3 overloads of the Tree-operator in Gremlinq:

  • Tree() will map to a Tree<object>. This is the quickest way to collect the traversers and their paths into a tree structure. Note that although the signature only defines the nodes to be of type object, upon deserialization, Gremlinq will usually be able to recognize elements (vertices and edges) and create instances accordingly.

    C#
    var untypedTree = await _g
        .V<Airport>()
        .Where(a => a.Code == "SEA")
        .Out<Route>()
        .Out<Route>()
        .Tree()
        .FirstAsync();
    
  • Tree<TNode>() will deserialize to a tree where all the nodes are known to be of type TNode.

    C#
    var typedTree = await _g
        .V<Airport>()
        .Where(a => a.Code == "SEA")
        .Out<Route>()
        .Out<Route>()
        .Tree<Airport>()
        .FirstAsync();
    
  • Precise type information of the tree nodes might be desired for later processing. Unfortunately, Gremlinq can't record all the types encountered on a path in its type system, so this information is not statically available. It can, however, be provided throgh call a Tree-overload that provides a special builder-DSL:

    C#
    var typedTreeWithOf = await _g
        .V<Airport>()
        .OutE<Route>()
        .InV<Airport>()
        .Tree(_ => _
            .Of<Airport>()
            .Of<Route>()
            .Of<Airport>())
        .FirstAsync();
    

    It's also possible to map to members which will change the node types in the resulting tree

    C#
    var typedTreeWithOfAndBy = await _g
        .V<Airport>()
        .Out<Route>()
        .OfType<Airport>()
        .Tree(_ => _
            .Of<Airport>().By(x => x.Code!)
            .Of<Airport>().By(x => x.Description!))
        .FirstAsync();