Schemaless GraphQL query builder for .NET with fluent syntax. Zero dependencies.
dotnet add package NGql.CoreNGql provides two powerful approaches for building GraphQL queries:
- Classic API - Direct query construction with nested objects
- QueryBuilder API - Modern fluent interface with advanced features
Both approaches support .NET 6.0, 7.0, 8.0, and 9.0.
The original NGql API for direct query construction.
var query = new Query("PersonAndFilms")
.Select(new Query("person")
.Where("id", "cGVvcGxlOjE=")
.Select("name")
.Select(new Query("filmConnection")
.Select(new Query("films")
.Select("title")))
);Output:
query PersonAndFilms{
person(id:"cGVvcGxlOjE="){
name
filmConnection{
films{
title
}
}
}
}var mutation = new Mutation("CreateUser")
.Select(new Query("createUser")
.Where("name", "Name")
.Where("password", "Password")
.Select("id", "name"));Output:
mutation CreateUser{
createUser(name:"Name", password:"Password"){
id
name
}
}var variable = new Variable("$name", "String");
var query = new Query("GetUser", variables: variable)
.Select(new Query("user")
.Where("name", variable)
.Select("id", "name"));Output:
query GetUser($name:String){
user(name:$name){
id
name
}
}The modern fluent API with advanced features for complex query construction.
// New QueryBuilder API
var query = QueryBuilder
.CreateDefaultBuilder("GetUsers")
.AddField("users.name")
.AddField("users.email");Output:
query GetUsers{
users{
name
email
}
}var query = QueryBuilder
.CreateDefaultBuilder("ComplexQuery")
.AddField("user.profile.name")
.AddField("user.profile.avatar.url")
.AddField("user.posts.title")
.AddField("user.posts.comments.author");Output:
query ComplexQuery{
user{
profile{
name
avatar{
url
}
}
posts{
title
comments{
author
}
}
}
}Specify field types directly in the field path:
var query = QueryBuilder
.CreateDefaultBuilder("TypedQuery")
.AddField("User user.profile")
.AddField("String user.name")
.AddField("Int user.age")
.AddField("[] user.tags") // Array marker
.AddField("Post[] user.posts"); // Typed arrayvar query = QueryBuilder
.CreateDefaultBuilder("UsersWithArgs")
.AddField("users", new Dictionary<string, object?>
{
{ "first", 10 },
{ "after", "cursor123" },
{ "orderBy", new EnumValue("CREATED_AT") }
})
.AddField("users.name")
.AddField("users.email");Output:
query UsersWithArgs{
users(first:10, after:"cursor123", orderBy:CREATED_AT){
name
email
}
}var query = QueryBuilder
.CreateDefaultBuilder("GetUserById")
.AddField("user", new Dictionary<string, object?>
{
{ "id", new Variable("$userId", "ID!") }
})
.AddField("user.name")
.AddField("user.email");Output:
query GetUserById($userId:ID!){
user(id:$userId){
name
email
}
}var query = QueryBuilder
.CreateDefaultBuilder("AliasedQuery")
.AddField("userName:user.name")
.AddField("userEmail:user.email")
.AddField("postTitles:user.posts.title");Output:
query AliasedQuery{
userName:user{
name
}
userEmail:user{
email
}
postTitles:user{
posts{
title
}
}
}var query = QueryBuilder
.CreateDefaultBuilder("SubFieldsQuery")
.AddField("user", subFields: ["name", "email"])
.AddField("user.posts", subFields: ["title", "content", "publishedAt"]);Output:
query SubFieldsQuery{
user{
name
email
posts{
title
content
publishedAt
}
}
}// Create reusable query fragments
var userFragment = QueryBuilder
.CreateDefaultBuilder("UserFragment")
.AddField("user.name")
.AddField("user.email")
.AddField("user.profile.avatar");
var postsFragment = QueryBuilder
.CreateDefaultBuilder("PostsFragment")
.AddField("user.posts.title")
.AddField("user.posts.publishedAt");
// Combine fragments
var combinedQuery = QueryBuilder
.CreateDefaultBuilder("CombinedQuery")
.Include(userFragment)
.Include(postsFragment);var query = QueryBuilder
.CreateDefaultBuilder("ComplexArgs")
.AddField("searchUsers", new Dictionary<string, object?>
{
{ "filter", new Dictionary<string, object?>
{
{ "name", new Variable("$name", "String") },
{ "age", new Dictionary<string, object?>
{
{ "gte", 18 },
{ "lte", 65 }
}
},
{ "status", new EnumValue("ACTIVE") }
}
},
{ "pagination", new
{
first = 20,
after = new Variable("$cursor", "String")
}
}
})
.AddField("searchUsers.edges.node.name")
.AddField("searchUsers.pageInfo.hasNextPage");var query = QueryBuilder
.CreateDefaultBuilder("FieldBuilderQuery")
.AddField("user", fieldBuilder =>
{
fieldBuilder.AddField("name")
.AddField("email")
.AddField("profile", profileBuilder =>
{
profileBuilder.AddField("bio")
.AddField("avatar");
});
});var query = QueryBuilder
.CreateDefaultBuilder("MetadataQuery")
.AddField("user.name", metadata: new Dictionary<string, object?>
{
{ "description", "User's display name" },
{ "required", true }
})
.WithMetadata(new Dictionary<string, object>
{
{ "version", "1.0" },
{ "author", "API Team" }
});One of the most powerful features of the QueryBuilder API is intelligent query merging. When combining multiple query fragments using .Include(), NGql can automatically merge compatible queries to optimize the final GraphQL output.
| Strategy | Description | Use Case |
|---|---|---|
MergeByDefault |
Inherits merging behavior from parent (default) | Most flexible, adapts to context |
MergeByFieldPath |
Merges queries with compatible field paths and arguments | Optimizing similar queries |
NeverMerge |
Always keeps queries separate | When you need guaranteed separation |
Automatically merges queries that have compatible field paths and arguments:
// Create root query with MergeByFieldPath strategy
var rootQuery = QueryBuilder
.CreateDefaultBuilder("OptimizedQuery", MergingStrategy.MergeByFieldPath)
.AddField("users", ["id", "name"]);
// Fragment 1: Same path, no arguments - WILL MERGE
var emailFragment = QueryBuilder
.CreateDefaultBuilder("EmailFragment")
.AddField("users", ["email"]);
// Fragment 2: Different path - WILL MERGE (compatible)
var profileFragment = QueryBuilder
.CreateDefaultBuilder("ProfileFragment")
.AddField("users.profile", ["bio", "avatar"]);
// Fragment 3: Same path but with arguments - WON'T MERGE
var filteredFragment = QueryBuilder
.CreateDefaultBuilder("FilteredFragment")
.AddField("users", new Dictionary<string, object?> { {"status", "active"} }, ["role"]);
var finalQuery = rootQuery
.Include(emailFragment) // Merges into main "users" field
.Include(profileFragment) // Merges as nested field
.Include(filteredFragment); // Creates separate field pathOutput:
query OptimizedQuery{
users{
id
name
email
profile{
bio
avatar
}
}
users_1(status:"active"){
role
}
}Forces queries to remain separate, even if they could be merged:
var rootQuery = QueryBuilder
.CreateDefaultBuilder("SeparateQueries", MergingStrategy.MergeByFieldPath)
.AddField("users", ["id"]);
// This fragment will NEVER merge due to NeverMerge strategy
var separateFragment = QueryBuilder
.CreateDefaultBuilder("AlwaysSeparate", MergingStrategy.NeverMerge)
.AddField("users", ["name", "email"]); // Same path but won't merge
var finalQuery = rootQuery.Include(separateFragment);Output:
query SeparateQueries{
users{
id
}
users_1{
name
email
}
}Inherits the merging behavior from the parent query:
// Root uses MergeByFieldPath
var rootQuery = QueryBuilder
.CreateDefaultBuilder("InheritedBehavior", MergingStrategy.MergeByFieldPath)
.AddField("users", ["id"]);
// Child uses MergeByDefault - will inherit MergeByFieldPath behavior
var childFragment = QueryBuilder
.CreateDefaultBuilder("ChildFragment", MergingStrategy.MergeByDefault)
.AddField("users", ["name"]); // Will merge because parent allows it
var finalQuery = rootQuery.Include(childFragment);Output:
query InheritedBehavior{
users{
id
name
}
}You can change merging strategies at runtime:
var query = QueryBuilder
.CreateDefaultBuilder("DynamicQuery")
.WithMergingStrategy(MergingStrategy.MergeByFieldPath)
.AddField("users.profile", ["name"]);
// Later change strategy
query.WithMergingStrategy(MergingStrategy.NeverMerge);// Root query optimizes by field path
var mainQuery = QueryBuilder
.CreateDefaultBuilder("ComplexMerging", MergingStrategy.MergeByFieldPath)
.AddField("organization.departments", ["id", "name"]);
// Fragment 1: Compatible path - WILL MERGE
var departmentDetails = QueryBuilder
.CreateDefaultBuilder("DeptDetails")
.AddField("organization.departments", ["budget", "headCount"]);
// Fragment 2: Nested compatible path - WILL MERGE
var teamInfo = QueryBuilder
.CreateDefaultBuilder("TeamInfo")
.AddField("organization.departments.teams", ["name", "lead"]);
// Fragment 3: Same path with arguments - WON'T MERGE
var activeDepartments = QueryBuilder
.CreateDefaultBuilder("ActiveDepts")
.AddField("organization.departments",
new Dictionary<string, object?> { {"status", "active"} },
["description"]);
// Fragment 4: Force separation - WON'T MERGE
var separateQuery = QueryBuilder
.CreateDefaultBuilder("Separate", MergingStrategy.NeverMerge)
.AddField("organization.departments", ["location"]);
var result = mainQuery
.Include(departmentDetails) // Merges
.Include(teamInfo) // Merges as nested
.Include(activeDepartments) // Separate due to arguments
.Include(separateQuery); // Separate due to NeverMergeOutput:
query ComplexMerging{
organization{
departments{
id
name
budget
headCount
teams{
name
lead
}
}
departments_1(status:"active"){
description
}
departments_2{
location
}
}
}- Field Path Compatibility: Queries merge if their field paths are compatible (same root or nested)
- Argument Matching: Queries with different arguments create separate field instances
- Strategy Hierarchy: Child strategies are overridden by parent
NeverMergestrategies - Type Safety: Conflicting field types prevent merging and throw exceptions
| Feature | Classic API | QueryBuilder API |
|---|---|---|
| Simple Query | new Query("users").Select("name") |
QueryBuilder.CreateDefaultBuilder("GetUsers").AddField("users.name") |
| Nested Fields | Multiple nested Query objects |
Dot notation: "user.profile.name" |
| Field Types | Not supported | "String user.name" or inline type specification |
| Arguments | .Where("id", value) |
.AddField("user", new Dictionary<string, object?> { {"id", value} }) |
| Aliases | Not directly supported | "alias:field" syntax |
| Query Merging | Manual composition | Automatic with .Include() and strategies |
| Variables | Constructor or .Variable() |
Automatic detection from arguments |
| Array Types | Not supported | "[]" and "Type[]" markers |
| Reusability | Limited | High with fragments and merging |
Before (Classic):
var query = new Query("GetUsers")
.Select(new Query("users")
.Select("name")
.Select("email"));After (QueryBuilder):
var query = QueryBuilder
.CreateDefaultBuilder("GetUsers")
.AddField("users.name")
.AddField("users.email");Before (Classic):
var query = new Query("GetUser")
.Select(new Query("user")
.Where("id", "123")
.Select("name"));After (QueryBuilder):
var query = QueryBuilder
.CreateDefaultBuilder("GetUser")
.AddField("user", new Dictionary<string, object?> { {"id", "123"} })
.AddField("user.name");Before (Classic):
var variable = new Variable("$id", "ID!");
var query = new Query("GetUser", variables: variable)
.Select(new Query("user")
.Where("id", variable)
.Select("name"));After (QueryBuilder):
var query = QueryBuilder
.CreateDefaultBuilder("GetUser")
.AddField("user", new Dictionary<string, object?>
{
{"id", new Variable("$id", "ID!")}
})
.AddField("user.name");var queryBuilder = QueryBuilder.CreateDefaultBuilder("DynamicQuery");
// Conditionally add fields
if (includeProfile)
{
queryBuilder.AddField("user.profile.name")
.AddField("user.profile.bio");
}
if (includePosts)
{
queryBuilder.AddField("user.posts.title")
.AddField("user.posts.publishedAt");
}
var query = queryBuilder.ToString();var query = QueryBuilder
.CreateDefaultBuilder("PathQuery")
.AddField("user.posts.comments.author");
// Get path to a specific query part
string[] pathToComments = query.GetPathTo("user", "posts.comments");
// Returns: ["user", "posts", "comments"]The QueryBuilder API includes several performance optimizations:
- Memory Efficient: Uses
Span<T>andReadOnlySpan<T>for reduced allocations - Field Caching: Intelligent caching of field definitions by path
- Optimized Parameters: Uses
inparameters for large data structures - Smart Merging: Efficient query merging with configurable strategies
- Use QueryBuilder for New Projects: The QueryBuilder API provides better maintainability and features
- Leverage Dot Notation: Use
"parent.child.field"syntax for cleaner code - Create Reusable Fragments: Build common query patterns as reusable components with
.Include() - Choose Appropriate Merging Strategies: Use
MergeByFieldPathfor optimization,NeverMergefor guaranteed separation - Type Your Fields: Use type annotations for better GraphQL schema compliance
- Use Variables: Prefer variables over hardcoded values for reusability
- Organize Complex Queries: Break large queries into smaller, composable fragments
Contributions are welcome! Please feel free to submit a Pull Request.
This project is licensed under the MIT License - see the LICENSE file for details.