- Notifications
You must be signed in to change notification settings - Fork20
C# GraphQL client with Linq-like syntax
License
byme8/ZeroQL
Folders and files
| Name | Name | Last commit message | Last commit date | |
|---|---|---|---|---|
Repository files navigation
🚀 Welcome to ZeroQL, a high-performance C#-friendly GraphQL client! 🎉
ZeroQL makes it easy to perform queries and mutations with Linq-like syntax. Unlike other GraphQL clients, ZeroQL doesn't require Reflection.Emit or expressions, which means the runtime provides performance very close to a raw HTTP call.
Here's a quick rundown of what ZeroQL can do at the moment:
- Bootstrap schema.graphql file from graphql endpoint
- Bootstrap client from schema.graphql file
- Support for queries and mutations
- "Lambda" like syntax
"Request" like syntaxDeprecated
- Support for subscriptions
- Support for fragments
- Support for interfaces
- Support for unions
- Support for scalars
- Support for file uploads
- Support for persisted queries
- Support for @defer
- Support for @stream
You can find the full wikihere or just by clicking on the feature bullet point you are interested in.
Check out our articles to learn more about ZeroQL:
- ZeroQL - C# friendly graphql client
- ZeroQL - C# GraphQL client adds fragments support
- ZeroQL V2
- ZeroQL V3
- ZeroQL V6
Here, you can find the setup for net6.0+ projects.You can findnetstandard or .Net Framework andUnity setup inwiki.
The initial setup:
# create console appdotnet new console -o QLClient# go to project foldercd QLClient# create manifest file to track nuget toolsdotnet new tool-manifest# add ZeroQL.CLI nuget tooldotnet tool install ZeroQL.CLI# or 'dotnet tool restore' once you pulled the existing repository# add ZeroQL nuget packagedotnet add package ZeroQL# fetch graphql schema from server(creates schema.graphql file)dotnet zeroql schema pull --url http://localhost:10000/graphql# to create ZeroQL config file: ./config.zeroql.jsondotnet zeroql config init# build the project to initiate the ZeroQL client generation with options specified inside config.zeroql.jsondotnet build
The build should be successful, and now we can use the generated client.
The commanddotnet zeroql config init creates theconfig.zeroql.json. By itself it looks like that:
{"$schema":"https://raw.githubusercontent.com/byme8/ZeroQL/main/schema.verified.json","graphql":"./schema.graphql","namespace":"ZeroQL.Client","clientName":"ZeroQLClient"}Now if you haveZeroQL package installed to yourcsproj, it will automatically detect and execute CLI based on this configuration file on every build. To make sure that it works, the config file should follow the*.zeroql.jsonpattern, or you can add a custom definition in yourcsproj like that:
<ItemGroup> <ZeroQLConfigInclude="you.custom.config.name.json"/></ItemGroup>
The generated client would be stored inside./obj/ZeroQL folder. So it will never appear in the solution. However, you still have access to generated classes in your source code.
If you want to turn off automatic generation on every build, it is possible to disable it:
<PropertyGroup> <ZeroQLOnBuildTriggerEnabled>False</ZeroQLOnBuildTriggerEnabled></PropertyGroup>
Let's suppose that schema.graphql file contains the following:
schema {query:Queriesmutation:Mutation}typeQueries {me:User!user(id:Int!):User}typeMutation {addUser(firstName:String!,lastName:String!):User!addUserProfileImage(userId:Int!file:Upload!):Int!}typeUser {id:Int!firstName:String!lastName:String!role:Role!}typeRole {id:Int!name:String!}
and we want to execute the query like that:
query {me {idfirstNamelastName } }
Here is how we can achieve it with ZeroQL "lambda" syntax:
varhttpClient=newHttpClient();httpClient.BaseAddress=newUri("http://localhost:10000/graphql");varclient=newTestServerGraphQLClient(httpClient);varresponse=awaitclient.Query(o=>o.Me(o=>new{o.Id,o.FirstName,o.LastName}));Console.WriteLine($"GraphQL:{response.Query}");// GraphQL: query { me { id firstName lastName } }Console.WriteLine($"{response.Data.Id}:{response.Data.FirstName}{response.Data.LastName}");// 1: Jon Smith
You can pass arguments inside lambda if needed:
varuserId=1;varresponse=awaitclient.Query(o=>o.User(userId, o=>newUser(o.Id,o.FirstName,o.LastName)));Console.WriteLine($"GraphQL:{response.Query}");// GraphQL: query ($id: Int!) { user(id: $id) { id firstName lastName } }Console.WriteLine($"{response.Data.Id}:{response.Data.FirstName}{response.Data.LastName}");// 1: Jon Smith
There is a limitation for lambda syntax. The variable should be a local variable or a parameter of the function.Otherwise, it will not be included in the lambda closure. As a result, ZeroQL would not be able to get a value.
Here is an example of the function parameter:
publicTask<User>GetUser(intuserId){varresponse=awaitclient.Query(o=>o.User(userId, o=>newUser(o.Id,o.FirstName,o.LastName)));returnresponse.Data;}
To be clear, you don't need actively account for it. ZeroQL will analyze and report errors if something is wrong.
For example, the next sample will not work:
publicintUserId{get;set;}publicTask<User>GetUser(){varresponse=awaitclient.Query(o=>o.User(UserId, o=>newUser(o.Id,o.FirstName,o.LastName)));// ZeroQL will report a compilation error herereturnresponse.Data;}
Also, there is a way to avoid lambda closure:
varvariables=new{Id=1};varresponse=awaitclient.Query(variables,static(i,o)=>o.User(i.Id, o=>newUser(o.Id,o.FirstName,o.LastName)));
You can fetch attached fields:
varvariables=new{Id=1};varresponse=awaitclient.Query(variables,static(i,o)=>o.User(i.Id, o=>new{o.Id,o.FirstName,o.LastName,Role=o.Role(role=>role.Name)}));Console.WriteLine($"GraphQL:{response.Query}");// GraphQL: query GetUserWithRole($id: Int!) { user(id: $id) { id firstName lastName role { name } } }Console.WriteLine($"{response.Data.Id}:{response.Data.FirstName}{response.Data.LastName}, Role:{response.Data.Role}");// 1: Jon Smith, Role: Admin
Request syntax is deprecated because it is not compatible with AOT runtimes and will be removed in future releases.
In more complex queries, the "lambda" syntax may look verbose, and extracting requests into a separate entity would be nice. Now it is possible to do it via the "request" syntax. Here is an example:
// define a requestpublicrecordGetUserQuery(intId):GraphQL<Queries,UserModel?>{publicoverrideUserModel?Execute(Queriesquery)=>query.User(Id, o=>newUserModel(o.Id,o.FirstName,o.LastName));}// execute a requestvarresponse=awaitclient.Execute(newGetUserQuery(variables.FriendId));Console.WriteLine(response.Query);// query GetUserQuery($id: Int!) { user(id: $id) { id firstName lastName } }Console.WriteLine(response.Data);// UserModel { Id = 2, FirstName = Ben, LastName = Smith }
You need to create a record from the base recordGraphQL<TOperationType, TResult>. Where theTOperationType is a root query type(Query,Mutation) that is associated with theGraphQLClient<TQuery, TMutataion> instance.~~
The complete benchmark source code you can findhere.
The short version looks like this:
[Benchmark]publicasyncTask<string>Raw(){varrawQuery=$$""" { "variables": { "id":{{id}} }, "query": "query GetUser($id: Int!){ user(id: $id) { id firstName lastName } }" }""";varresponse=awaithttpClient.PostAsync("",newStringContent(rawQuery,Encoding.UTF8,"application/json"));varresponseJson=awaitresponse.Content.ReadAsStreamAsync();varqlResponse=JsonSerializer.Deserialize<JsonObject>(responseJson,options);returnqlResponse!["data"]!["user"]!["firstName"]!.GetValue<string>();}[Benchmark]publicasyncTask<string>StrawberryShake(){// query GetUser($id: Int!) {// user(id: $id) {// id// firstName// lastName// }// }varfirstname=awaitstrawberryShake.GetUser.ExecuteAsync(id);returnfirstname.Data!.User!.FirstName;}[Benchmark]publicasyncTask<string>ZeroQLLambdaWithoutClosure(){varvariables=new{Id=id};varfirstname=awaitzeroQLClient.Query(variables,static(i,q)=>q.User(i.Id, o=>new{o.Id,o.FirstName,o.LastName}));returnfirstname.Data!.FirstName;}[Benchmark]publicasyncTask<string>ZeroQLLambdaWithClosure(){varid=this.id;varfirstname=awaitzeroQLClient.Query( q=>q.User(id, o=>new{o.Id,o.FirstName,o.LastName}));returnfirstname.Data!.FirstName;}[Benchmark]publicasyncTask<string>ZeroQLRequest(){varfirstname=awaitzeroQLClient.Execute(newGetUserQuery(id));returnfirstname.Data!.FirstName;}// ..publicrecordGetUserQuery(intid):GraphQL<Query,User?>{publicoverrideUser?Execute(Queryquery)=>query.User(id, o=>newUser(o.Id,o.FirstName,o.LastName));}
Here results:
BenchmarkDotNet v0.15.8, macOS Tahoe 26.1 (25B78) [Darwin 25.1.0]Apple M3 Max, 1 CPU, 14 logical and 14 physical cores.NET SDK 10.0.100 [Host] : .NET 10.0.0 (10.0.0, 10.0.25.52411), Arm64 RyuJIT armv8.0-a DefaultJob : .NET 10.0.0 (10.0.0, 10.0.25.52411), Arm64 RyuJIT armv8.0-a
| Method | Mean | Error | StdDev | Gen0 | Allocated |
|---|---|---|---|---|---|
| Raw | 62.29 μs | 0.504 μs | 0.421 μs | 0.4883 | 5.13 KB |
| StrawberryShake | 65.10 μs | 0.362 μs | 0.302 μs | 1.3428 | 11.22 KB |
| ZeroQLLambdaWithoutClosure | 63.91 μs | 1.203 μs | 1.181 μs | 0.8545 | 6.9 KB |
| ZeroQLLambdaWithClosure | 65.59 μs | 1.261 μs | 1.595 μs | 0.8545 | 7.38 KB |
| ZeroQLRequest | 64.49 μs | 1.244 μs | 1.163 μs | 0.7324 | 6.48 KB |
As you can see, theRaw method is the fastest.TheZeroQL method is a bit faster than theStrawberryShake method.But in absolute terms, all of them are pretty much the same.
So, with theZeroQL, you can forget about the graphql and just use the Linq-like interface.It will have little effect on performance.
The initial inspiration for this project came from the work done athttps://github.com/Giorgi/GraphQLinq
About
C# GraphQL client with Linq-like syntax
Topics
Resources
License
Uh oh!
There was an error while loading.Please reload this page.
Stars
Watchers
Forks
Uh oh!
There was an error while loading.Please reload this page.
Contributors14
Uh oh!
There was an error while loading.Please reload this page.