This guide demonstrates how to use the GraphRAG LINQ extensions with Entity Framework Core (and the new SharpCoreDB.Functional.Linq2DB adapter) to query graph relationships with a fluent, type-safe API.
New: For zero-overhead, compile-time safe queries with railway-oriented return types (Option<T>, Fin<T>, Seq<T>), use the production-ready SharpCoreDB.Functional.Linq2DB package. It pairs excellently with GraphRAG patterns for agentic/AI workloads.
Phase Status: ✅ Phase 2 complete (Phase 3 prototype)
- LINQ Graph Queries:
.Traverse()method for fluent graph exploration - Strategy Support: BFS, DFS, Bidirectional, and Dijkstra traversal
- Depth Control: Set maximum traversal depth
- Filtering: Combine graph traversal with WHERE predicates
- Performance: Translates to
GRAPH_TRAVERSE()SQL functions - Type-Safe: Full IntelliSense support in Visual Studio
// Traverse from node 1, following the "next" relationship
var nodeIds = await db.Nodes
.Traverse(startNodeId: 1, relationshipColumn: "next",
maxDepth: 3, strategy: GraphTraversalStrategy.Bfs)
.ToListAsync();
// Result: IEnumerable<long> containing all reachable node IDs// Find all orders linked to nodes within 5 hops of node 1
var traversalIds = new List<long> { 1, 2, 3, 4, 5 };
var orders = await db.Orders
.WhereIn(traversalIds)
.ToListAsync();
// Alternative: Use TraverseWhere for combined query
var orders = await db.Orders
.TraverseWhere(
startNodeId: 1,
relationshipColumn: "NodeId",
maxDepth: 5,
strategy: GraphTraversalStrategy.Dfs,
predicate: o => o.Amount > 100)
.ToListAsync();// Multi-step graph exploration with filtering
var result = await db.Orders
.Where(o => db.Nodes
.Traverse(startNodeId: o.LinkedNodeId,
relationshipColumn: "parent",
maxDepth: 3,
strategy: GraphTraversalStrategy.Bfs)
.Contains(o.RootNodeId))
.Where(o => o.Status == "Active")
.OrderBy(o => o.Amount)
.ToListAsync();Traverses a graph and returns reachable node IDs.
public static IQueryable<long> Traverse<TEntity>(
this IQueryable<TEntity> source,
long startNodeId,
string relationshipColumn,
int maxDepth,
GraphTraversalStrategy strategy)Parameters:
startNodeId: The row ID to start traversal fromrelationshipColumn: Name of the ROWREF column containing edge relationshipsmaxDepth: Maximum number of hops (0 = only start node, 1 = start + neighbors, etc.)strategy:GraphTraversalStrategy.Bfs,Dfs,Bidirectional, orDijkstra
Returns: IQueryable<long> - Database-evaluated traversal results
Filters entities by checking if their ID is in the traversal result set.
public static IQueryable<TEntity> WhereIn<TEntity>(
this IQueryable<TEntity> source,
IEnumerable<long> traversalIds)Parameters:
traversalIds: Collection of IDs to filter by
Returns: Filtered IQueryable<TEntity>
Example:
var nodes = new List<long> { 1, 2, 3, 4, 5 };
var entities = await db.MyEntities
.WhereIn(nodes)
.ToListAsync();Combines graph traversal with an additional WHERE predicate in a single query.
public static IQueryable<TEntity> TraverseWhere<TEntity>(
this IQueryable<TEntity> source,
long startNodeId,
string relationshipColumn,
int maxDepth,
GraphTraversalStrategy strategy,
Expression<Func<TEntity, bool>> predicate)Example:
var expensiveOrders = await db.Orders
.TraverseWhere(
startNodeId: 1,
relationshipColumn: "supplierNodeId",
maxDepth: 4,
strategy: GraphTraversalStrategy.Bfs,
predicate: o => o.Amount > 1000)
.ToListAsync();Removes duplicate IDs from traversal results.
var unique = await db.Nodes
.Traverse(1, "next", 5, GraphTraversalStrategy.Bfs)
.Distinct()
.ToListAsync();Limits the number of traversal results returned.
var first10 = await db.Nodes
.Traverse(1, "next", 10, GraphTraversalStrategy.Bfs)
.Take(10)
.ToListAsync();Explores nodes level by level, guaranteeing shortest paths.
.Traverse(1, "next", 5, GraphTraversalStrategy.Bfs)Best for:
- Finding nearest neighbors
- Shortest path analysis
- Level-based exploration
- Knowledge graphs
Explores as far as possible along each branch before backtracking.
.Traverse(1, "parent", 5, GraphTraversalStrategy.Dfs)Best for:
- Tree-like hierarchies
- Deep relationship chains
- Memory-efficient exploration
- Hierarchical data
Explores outgoing and incoming relationships.
.Traverse(1, "related", 3, GraphTraversalStrategy.Bidirectional)Best for:
- Undirected graphs modeled with ROWREF
- Finding neighbors across incoming edges
Weighted shortest paths (uses edge-table weight when present).
.Traverse(1, "weightedNext", 10, GraphTraversalStrategy.Dijkstra)Best for:
- Weighted graph analysis
- Routing with edge weights
The LINQ methods compile to efficient SQL using the GRAPH_TRAVERSE() function:
var result = db.Nodes
.Traverse(1, "nextId", 3, GraphTraversalStrategy.Bfs)
.ToListAsync();Generates SQL:
SELECT GRAPH_TRAVERSE(1, 'nextId', 3, 0)var result = db.Orders
.Where(o => db.Nodes
.Traverse(1, "supplierId", 5, GraphTraversalStrategy.Bfs)
.Contains(o.NodeId))
.Where(o => o.Amount > 100)
.ToListAsync();Generates SQL:
SELECT * FROM Orders
WHERE NodeId IN (GRAPH_TRAVERSE(1, 'supplierId', 5, 0))
AND Amount > 100var result = db.Orders
.WhereIn(traversalIds)
.Where(o => o.Status == "Active")
.OrderBy(o => o.CreatedDate)
.ToListAsync();Generates SQL:
SELECT * FROM Orders
WHERE Id IN (traversal_ids)
AND Status = 'Active'
ORDER BY CreatedDate ASC- Database Evaluation: All traversal logic runs in the database via
GRAPH_TRAVERSE()SQL function - No Network Overhead: Results streamed directly from database
- Index Utilization: Native SQL queries leverage existing indexes
- Lazy Evaluation: LINQ queries are not executed until
.ToList()or.ToListAsync()
using SharpCoreDB.Functional.Linq2DB;
using static SharpCoreDB.Functional.Prelude;
var conn = new SharpCoreDBDataConnection("Data Source=./graphrag.scdb");
var db = new FunctionalLinq2DbContext(conn);
// Functional + type-safe LINQ (perfect for GraphRAG agent flows)
var node = await db.FindOneAsync<Node>(n => n.Id == 1);
var neighbors = await db.QueryAsync<Node>(q => q
.Where(n => n.Type == "Entity")
.OrderBy(n => n.Name)
.Take(10));See src/SharpCoreDB.Functional.Linq2DB/README.md for full API (InsertBatchAsync with BulkCopy, transactions, etc.).
-
Use
.ToListAsync()in async contextsvar results = await query.ToListAsync(); // ✅ Correct var results = query.ToList().Result; // ❌ Blocking
-
Limit depth for large graphs
.Traverse(1, "next", 5, ...) // ✅ Reasonable .Traverse(1, "next", 1000, ...) // ❌ Expensive
-
Apply filtering early
db.Orders .Where(o => o.Status == "Active") // ✅ Filter first .WhereIn(traversalIds) // Then traverse
-
Reuse traversal results
var ids = await query.ToListAsync(); // Reuse ids for multiple subsequent queries
// ❌ Negative max depth
db.Nodes.Traverse(1, "next", -1, GraphTraversalStrategy.Bfs);
// Throws: ArgumentOutOfRangeException
// ❌ Null relationship column
db.Nodes.Traverse(1, null, 3, GraphTraversalStrategy.Bfs);
// Throws: ArgumentException
// ❌ Null source
IQueryable<Node> source = null;
source.Traverse(1, "next", 3, GraphTraversalStrategy.Bfs);
// Throws: ArgumentNullExceptiontry
{
var result = await db.Nodes
.Traverse(startNodeId, relationshipColumn, maxDepth, strategy)
.ToListAsync(cancellationToken);
}
catch (ArgumentException ex)
{
// Handle invalid parameters
logger.LogError(ex, "Invalid traversal parameters");
}
catch (OperationCanceledException)
{
// Handle cancellation
logger.LogInformation("Traversal cancelled");
}// Get all employees under a manager (hierarchical structure)
var subordinates = await db.Employees
.Where(e => db.Employees
.Traverse(startNodeId: managerId,
relationshipColumn: "supervisorId",
maxDepth: 10, // Max organizational depth
strategy: GraphTraversalStrategy.Bfs)
.Contains(e.Id))
.ToListAsync();// Locate all products obtainable from a supplier
var products = await db.Products
.Where(p => db.SupplyChain
.Traverse(startNodeId: supplierId,
relationshipColumn: "sourceId",
maxDepth: 5,
strategy: GraphTraversalStrategy.Bfs)
.Contains(p.SourceNodeId))
.Where(p => p.InStock)
.OrderBy(p => p.Price)
.ToListAsync();// Find friends of friends (degree-2 network)
var potentialFriends = await db.Users
.Where(u => db.Friendships
.Traverse(startNodeId: userId,
relationshipColumn: "friendId",
maxDepth: 2,
strategy: GraphTraversalStrategy.Bfs)
.Contains(u.Id))
.Where(u => !db.BlockedUsers.Any(b => b.UserId == userId && b.BlockedId == u.Id))
.OrderByDescending(u => u.MutualFriendCount)
.Take(20)
.ToListAsync();// Find all related concepts
var relatedConcepts = await db.Concepts
.Where(c => db.ConceptGraph
.Traverse(startNodeId: conceptId,
relationshipColumn: "relatedConceptId",
maxDepth: 3,
strategy: GraphTraversalStrategy.Dijkstra) // Use weighted edges
.Contains(c.Id))
.OrderBy(c => c.Relevance)
.ToListAsync();Cause: Database provider not correctly configured
Solution: Ensure DbContextOptions uses .UseSharpCoreDB(connectionString)
Cause: Wrong relationship column name
Solution: Verify ROWREF column name in table schema
Cause: Large max depth or missing indexes
Solution:
- Reduce
maxDepthparameter - Ensure ROWREF column is indexed
- Use DFS instead of BFS for deep graphs