Skip to the content.

Migration Guide: v2.x to v3.0

This guide covers all breaking changes and new features introduced in v3.0.

Breaking Changes

1. Query<T> renamed to GraphQLField<T>

The Query<T> class and its interfaces have been renamed to better reflect their purpose — they build field selections, not full operations.

v2.x v3.0
IQuery IGraphQLField
IQuery<T> IGraphQLField<T>
Query<T> GraphQLField<T>

Before:

IQuery<User> query = new Query<User>("user")
    .AddField(u => u.Name);

After:

IGraphQLField<User> query = new GraphQLField<User>("user")
    .AddField(u => u.Name);

2. Null properties are now included by default

Previously, null properties were silently dropped when using AddArguments<T>() or when serializing objects as argument values. Now they are included as null.

Before (v2.x):

new Query<object>("car").AddArguments(new Car { Name = "Bee", Price = 10000 });
// Color and Manufacturer (both null) were silently dropped
// Output: car(Name:"Bee",Price:10000)

After (v3.0):

new GraphQLField<object>("car").AddArguments(new Car { Name = "Bee", Price = 10000 });
// Null properties are now included
// Output: car(Color:null,Manufacturer:null,Name:"Bee",Price:10000)

To restore the old behavior, set DefaultIgnoreCondition:

var options = new QueryOptions
{
    DefaultIgnoreCondition = QueryIgnoreCondition.WhenWritingNull
};

new GraphQLField<object>("car", options).AddArguments(new Car { Name = "Bee", Price = 10000 });
// Output: car(Name:"Bee",Price:10000)

Available modes:

Value Behavior
QueryIgnoreCondition.Never Include all properties (new default)
QueryIgnoreCondition.WhenWritingNull Skip properties with null values (old default)
QueryIgnoreCondition.WhenWritingDefault Skip properties with default values (null, 0, false, etc.)

3. Duplicate argument keys now overwrite instead of throwing

AddArgument previously threw ArgumentException on duplicate keys. Now it silently overwrites the value.

Before (v2.x):

query.AddArgument("id", 1);
query.AddArgument("id", 2); // throws ArgumentException

After (v3.0):

query.AddArgument("id", 1);
query.AddArgument("id", 2); // overwrites, id = 2

4. Names are now validated

Every string that ends up in the query as an identifier is validated against the GraphQL identifier pattern [a-zA-Z_][a-zA-Z0-9_]*: the GraphQLField<T> name, Alias, AddField(string), AddArgument(string, ...), AddUnion(string, ...), the operation name, directive names and directive argument names, fragment names and type names, and variable names. Variable types are validated as GraphQL type references, so brackets and ! are allowed there (ID!, [String!]!).

query.AddField("valid_name");   // OK
query.AddField("__typename");   // OK
query.AddField("field-name");   // throws ArgumentException
query.AddField("123field");     // throws ArgumentException

new GraphQLVariable("id", "[ID!]!");   // OK
new GraphQLVariable("id", "[ID!");     // throws ArgumentException

This does not affect lambda-based AddField(x => x.Property) calls — C# property names are inherently valid.

5. String escaping is now more thorough

Strings in argument values now escape backslashes, newlines, carriage returns, and tabs in addition to double quotes.

Character v2.x v3.0
" \" \"
\ (not escaped) \\
newline (not escaped) \n
carriage return (not escaped) \r
tab (not escaped) \t

New Features

Operation Wrapper

Wrap field selections in a full GraphQL operation with type and name:

string result = new GraphQLOperation(OperationType.Query, "GetUser")
    .AddQuery(
        new GraphQLField<User>("user")
            .AddArgument("id", 1)
            .AddField(u => u.Name))
    .Build();
// => "query GetUser{user(id:1){Name}}"

Supports Query, Mutation, Subscription, and shorthand (no type):

// Mutation
new GraphQLOperation(OperationType.Mutation)

// Subscription
new GraphQLOperation(OperationType.Subscription)

// Shorthand — omits the type keyword
new GraphQLOperation()

Multiple Root Fields

Compose multiple fields in a single operation:

string result = new GraphQLOperation(OperationType.Query)
    .AddQuery(new GraphQLField<User>("user").AddArgument("id", 1).AddField(u => u.Name))
    .AddQuery(new GraphQLField<Post>("posts").AddField(p => p.Title))
    .Build();
// => "query{user(id:1){Name} posts{Title}}"

Variables

Define typed variables at the operation level and reference them in arguments:

var idVar = new GraphQLVariable("id", "ID!");
var limitVar = new GraphQLVariable("limit", "Int", 10); // with default value

string result = new GraphQLOperation(OperationType.Query, "GetUser")
    .AddVariable(idVar)
    .AddVariable(limitVar)
    .AddQuery(
        new GraphQLField<User>("user")
            .AddArgument("id", idVar.Reference())
            .AddArgument("limit", limitVar.Reference())
            .AddField(u => u.Name))
    .Build();
// => "query GetUser($id:ID!,$limit:Int=10){user(id:$id,limit:$limit){Name}}"

Directives

Attach @include, @skip, or custom directives to fields:

var showVar = new GraphQLVariable("show", "Boolean!");

// @include / @skip
new GraphQLField<User>("user")
    .AddField(u => u.Name, GraphQLDirective.Include(showVar.Reference()))
    .AddField(u => u.Age, GraphQLDirective.Skip(showVar.Reference()));

// Custom directive
var cached = new GraphQLDirective("cacheControl", new Dictionary<string, object?>
{
    { "maxAge", 300 }
});
new GraphQLField<User>("user")
    .AddField(u => u.Name, cached);
// => "user{Name @cacheControl(maxAge:300)}"

// Directive on a sub-query
new GraphQLField<Car>("car")
    .AddField(
        c => c.Color,
        sq => sq.AddField(color => color!.Red).AddField(color => color!.Green),
        GraphQLDirective.Include(showVar.Reference()));
// => "car{Color @include(if:$show){Red Green}}"

Fragments

Define reusable field selections and spread them across queries:

var userFields = new GraphQLFragment("UserFields", "User",
    new GraphQLField<User>("User")
        .AddField(u => u.Name)
        .AddField(u => u.Email));

string result = new GraphQLOperation(OperationType.Query)
    .AddQuery(
        new GraphQLField<User>("user")
            .AddArgument("id", 1)
            .AddFragment(userFields))
    .AddQuery(
        new GraphQLField<User>("admin")
            .AddArgument("id", 2)
            .AddFragment(userFields))
    .AddFragment(userFields)
    .Build();
// => "query{user(id:1){...UserFields} admin(id:2){...UserFields}} fragment UserFields on User{Name Email}"