NGql.Core 1.5.1-alpha.0.3

This is a prerelease version of NGql.Core.
There is a newer prerelease version of this package available.
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
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="NGql.Core" Version="1.5.1-alpha.0.3" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="NGql.Core" Version="1.5.1-alpha.0.3" />
                    
Directory.Packages.props
<PackageReference Include="NGql.Core" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add NGql.Core --version 1.5.1-alpha.0.3
                    
#r "nuget: NGql.Core, 1.5.1-alpha.0.3"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package NGql.Core@1.5.1-alpha.0.3
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=NGql.Core&version=1.5.1-alpha.0.3&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=NGql.Core&version=1.5.1-alpha.0.3&prerelease
                    
Install as a Cake Tool

Project Logo

NGql

Schemaless GraphQL query builder for .NET with fluent syntax. Zero dependencies.

GitHub license

Quick Start

dotnet add package NGql.Core

Overview

NGql provides two powerful approaches for building GraphQL queries:

  1. Classic API - Direct query construction with nested objects
  2. 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

  1. Field Path Compatibility: Queries merge if their field paths are compatible (same root or nested)
  2. Argument Matching: Queries with different arguments create separate field instances
  3. Strategy Hierarchy: Child strategies are overridden by parent NeverMerge strategies
  4. 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> and ReadOnlySpan<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

  1. Use QueryBuilder for New Projects: The QueryBuilder API provides better maintainability and features
  2. Leverage Dot Notation: Use "parent.child.field" syntax for cleaner code
  3. Create Reusable Fragments: Build common query patterns as reusable components with .Include()
  4. Choose Appropriate Merging Strategies: Use MergeByFieldPath for optimization, NeverMerge for guaranteed separation
  5. Type Your Fields: Use type annotations for better GraphQL schema compliance
  6. Use Variables: Prefer variables over hardcoded values for reusability
  7. 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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • 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