NGql.Core
1.5.1-alpha.0.3
See the version list below for details.
dotnet add package NGql.Core --version 1.5.1-alpha.0.3
NuGet\Install-Package NGql.Core -Version 1.5.1-alpha.0.3
<PackageReference Include="NGql.Core" Version="1.5.1-alpha.0.3" />
<PackageVersion Include="NGql.Core" Version="1.5.1-alpha.0.3" />
<PackageReference Include="NGql.Core" />
paket add NGql.Core --version 1.5.1-alpha.0.3
#r "nuget: NGql.Core, 1.5.1-alpha.0.3"
#:package NGql.Core@1.5.1-alpha.0.3
#addin nuget:?package=NGql.Core&version=1.5.1-alpha.0.3&prerelease
#tool nuget:?package=NGql.Core&version=1.5.1-alpha.0.3&prerelease
NGql
Schemaless GraphQL query builder for .NET with fluent syntax. Zero dependencies.
Quick Start
dotnet add package NGql.Core
Overview
NGql 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.
Classic API
The original NGql API for direct query construction.
Basic Query
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
}
}
}
}
Mutation
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
}
}
Variables
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
}
}
QueryBuilder API ✨
The modern fluent API with advanced features for complex query construction.
Basic Usage
Simple Fields
// New QueryBuilder API
var query = QueryBuilder
.CreateDefaultBuilder("GetUsers")
.AddField("users.name")
.AddField("users.email");
Output:
query GetUsers{
users{
name
email
}
}
Multiple Fields with Dot Notation
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
}
}
}
}
Advanced Features
Field Types with Dot Notation
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 array
Field Arguments
var 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
}
}
Variables
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
}
}
Field Aliases
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
}
}
}
SubFields Syntax
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
}
}
}
Query Composition and Merging
// 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);
Complex Arguments with Nested Objects
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");
Field Builder Actions
var query = QueryBuilder
.CreateDefaultBuilder("FieldBuilderQuery")
.AddField("user", fieldBuilder =>
{
fieldBuilder.AddField("name")
.AddField("email")
.AddField("profile", profileBuilder =>
{
profileBuilder.AddField("bio")
.AddField("avatar");
});
});
Metadata Support
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" }
});
Query Merging Strategies 🔄
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.
Available Strategies
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 |
1. MergeByFieldPath Strategy
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 path
Output:
query OptimizedQuery{
users{
id
name
email
profile{
bio
avatar
}
}
users_1(status:"active"){
role
}
}
2. NeverMerge Strategy
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
}
}
3. MergeByDefault Strategy
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
}
}
Dynamic Strategy Assignment
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);
Complex Merging Example
// 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 NeverMerge
Output:
query ComplexMerging{
organization{
departments{
id
name
budget
headCount
teams{
name
lead
}
}
departments_1(status:"active"){
description
}
departments_2{
location
}
}
}
Merging Rules
- 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
NeverMerge
strategies - Type Safety: Conflicting field types prevent merging and throw exceptions
Comparison: Classic vs QueryBuilder
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 |
Migration Guide
Simple Query Migration
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");
Arguments Migration
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");
Variables Migration
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");
Advanced Scenarios
Dynamic Query Building
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();
Path Resolution
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"]
Performance Optimizations
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
in
parameters for large data structures - Smart Merging: Efficient query merging with configurable strategies
Best Practices
- 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
MergeByFieldPath
for optimization,NeverMerge
for 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
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
License
This project is licensed under the MIT License - see the LICENSE file for details.
Product | Versions Compatible and additional computed target framework versions. |
---|---|
.NET | net6.0 is compatible. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 is compatible. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net6.0
- No dependencies.
-
net7.0
- No dependencies.
-
net8.0
- No dependencies.
-
net9.0
- No dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
Version | Downloads | Last Updated |
---|---|---|
1.5.1-alpha.0.13 | 109 | 9/2/2025 |
1.5.1-alpha.0.3 | 160 | 8/30/2025 |
1.5.0 | 194 | 8/20/2025 |
1.5.0-preview.0.19 | 114 | 8/20/2025 |
1.5.0-preview.0.18 | 110 | 8/20/2025 |
1.5.0-preview.0.17 | 163 | 8/14/2025 |
1.5.0-preview.0.14 | 117 | 8/13/2025 |
1.5.0-preview.0.13 | 117 | 8/13/2025 |
1.5.0-preview.0.12 | 115 | 8/13/2025 |
1.5.0-preview.0.11 | 116 | 8/13/2025 |
1.5.0-preview.0.8 | 112 | 8/11/2025 |
1.5.0-preview.0.7 | 193 | 8/7/2025 |
1.5.0-preview.0.6 | 193 | 8/7/2025 |
1.5.0-preview.0.5 | 192 | 8/6/2025 |
1.5.0-preview.0.4 | 196 | 8/6/2025 |
1.5.0-preview.0.3 | 198 | 8/5/2025 |
1.5.0-preview.0.2 | 194 | 8/5/2025 |
1.5.0-preview.0.1 | 205 | 8/5/2025 |
1.4.1 | 100 | 7/30/2025 |
1.4.0 | 176 | 5/30/2024 |
1.3.3 | 3,344 | 5/10/2024 |
1.3.2 | 141 | 5/10/2024 |
1.3.1 | 188 | 5/9/2024 |
1.3.0 | 154 | 5/8/2024 |
1.2.0 | 279 | 3/24/2024 |
1.1.1 | 4,932 | 10/16/2023 |
1.1.0 | 2,711 | 10/3/2023 |
1.0.0 | 18,058 | 11/28/2021 |
0.1.6 | 1,370 | 11/19/2021 |
0.1.5 | 1,599 | 10/22/2021 |
0.1.3 | 1,457 | 10/11/2021 |
0.1.2 | 1,018 | 9/15/2021 |
0.1.1 | 869 | 9/9/2021 |
0.1.0 | 867 | 9/9/2021 |