发布于 2026-01-06 7 阅读
0

ZeroQL - C# 友好的 GraphQL

ZeroQL - C# 友好的 GraphQL

如今,GraphQL 已成为构建 Web 服务器越来越流行的技术。然而,C# 却缺少一个“原生”的 GraphQL 客户端。我所说的“原生”是指能够在 C# 内部构建查询,而无需编写原始的 GraphQL 代码,并确保项目编译通过后就能按预期运行。

这个想法

我一直在寻找这样的工具,最终找到的最接近的就是Strawberry Shake。它需要你编写原始的 GraphQL 代码。同时,它会自动生成所有必要的 C# 封装,让你拥有一个简洁且类型安全的 API。

我一直在使用它,但想要一个更原生化的方案来简化不同应用组件之间的集成。
我的目标是提供一个公共接口,允许执行类似这样的查询:

var response = await client.Query(q => q.User(42, user => new { user.Id, user.FirstName, user.LastName });
Enter fullscreen mode Exit fullscreen mode

它等价于以下 GraphQL 查询:

query { user(id: 42) { id firstName lastName } }
Enter fullscreen mode Exit fullscreen mode

ZeroQL

经过几周的研究,我创建了一个可以实现这一功能的库。

隆重推出ZeroQL!它是一款对 C# 友好的 GraphQL 客户端,具有类似 Linq 的接口和与简单 HTTP 调用相当的出色性能。

让我们通过一个例子来看一下它的实际应用。假设我们在 localhost:10000 上有一个本地HotChocolate服务器,它提供以下 GraphQL schema:

schema {
  query: Query
  mutation: Mutation
}

type Query {
  me: User!
  user(id: Int!): User
}

type Mutation {
  addUser(firstName: String!, lastName: String!): User!
}

type User {
  id: Int!
  firstName: String!
  lastName: String!
  role: Role!
}

type Role {
  id: Int!
  name: String!
}
Enter fullscreen mode Exit fullscreen mode

初始设置

现在,我们来创建一个可以访问它的控制台应用程序。我们可以使用以下命令来实现:

dotnet new console -o QLClient # create console app
cd QLClient # go to the project folder
curl http://localhost:10000/graphql?sdl > schema.graphql # fetch graphql schema from server
dotnet new tool-manifest # create manifest file to track NuGet tools
dotnet tool install ZeroQL.CLI # add ZeroQL.CLI NuGet tool
dotnet add package ZeroQL # add ZeroQL NuGet package
dotnet zeroql generate --schema .\schema.graphql --namespace TestServer.Client --client-name TestServerGraphQLClient --output Generated/GraphQL.g.cs # generate wrappers from the schema.graphql
Enter fullscreen mode Exit fullscreen mode

最后一步可以放在 csproj 文件中的一个单独目标中,以确保我们能够获取 schama.graphql 的最新更改。它可能看起来像这样:

<Target Name="GenerateQLClient" BeforeTargets="BeforeCompile">
    <Exec Command="dotnet zeroql generate --schema .\schema.graphql --namespace TestServer.Client --client-name TestServerGraphQLClient --output Generated/GraphQL.g.cs" />
</Target>
Enter fullscreen mode Exit fullscreen mode

乍一看可能有点复杂,但别担心,我们只需要做一次。

初始设置已完成,我们可以执行第一个查询了。让我们修改 Program.cs 文件,使其如下所示:

var httpClient = new HttpClient();
httpClient.BaseAddress = new Uri("http://localhost:10000/graphql");

var client = new TestServerGraphQLClient(httpClient);

var response = await client.Query(static q => q.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
Enter fullscreen mode Exit fullscreen mode

如您所见,工作流程非常简单。创建 GraphQL 客户端,用 C# 编写查询,执行查询并获取结果。让我们详细了解一下这个示例。

工作原理

该类TestServerGraphQLClient是通过 ZeroQL.CLI 生成的。它有一个方法,Query该方法接受一个“graphql”lambda* (不是表达式) *。这个“graphql”lambda 接受一个类型为 的参数Query。它也是生成的。然后,源代码生成器会查看 lambda 内部,进行分析,并将其转换为相应的 GraphQL。之后,它会被放入一个“特殊”的字典中。这个字典包含字符串化的 lambda 及其关联的 GraphQL。如果您查看该Query方法本身,您会发现它有一个隐藏的参数queryKey

public async Task<GraphQLResult<TResult>> Query<TResult>(
    Func<TQuery, TResult> query,
    [CallerArgumentExpression("query")] string queryKey = null!)
{
    return await Execute<Unit, TQuery, TResult>(OperationKind.Query, null, null, (i, q) => query(q), queryKey);
}
Enter fullscreen mode Exit fullscreen mode

CallerArgumentExpression是 C# 10 的一项新特性。它允许我们获取参数中传递的表达式的字符串化表示。在我们的例子中,我们正在寻找query等于某个表达式的参数,static q => q.Me(o => new { o.Id, o.FirstName, o.LastName })因此该参数queryKey将包含硬编码的字符串"static q => q.Me(o => new { o.Id, o.FirstName, o.LastName })"——正是从“特殊”字典中获取相应 GraphQL 所需的精确表示。因此,我们始终知道每次调用所需的 GraphQL。关键在于,GraphQL 在编译时生成。因此,运行时除了执行 HTTP 调用之外,无需执行任何其他操作。因此,运行时开销为零。

另一点需要注意的是,“graphql”lambda 必须是静态的。原因有二。首先,通过源代码生成器分析它会容易得多,因为没有作用域外的变量会使情况变得复杂。其次,如果您计划使用类似这样的 graphql 变量:

var variables = new { Id = 1 };
var response = await client.Query(variables, static (i, q) => q.User(i.Id, o => new { o.Id, o.FirstName, o.LastName }));
Enter fullscreen mode Exit fullscreen mode

这是确保所有输入都被分析的最简单方法。通过这种方法,我们可以将它们作为参数,并将它们序列化以添加到请求中。

支持的功能

现在让我们来看看目前支持哪些功能。
例如,我们可以获取深度嵌套字段:

var variables = new { Id = 1 };
var response = await client.Query(
    variables,
    static (i, q) => q
        .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
Enter fullscreen mode Exit fullscreen mode

此外,还可以同时触摸多个字段:

var variables = new { Id = 1 };
var response = await client.Query(
    variables,
    static (i, q) => new
    {
        MyFirstName = q.Me(o => o.FirstName),
        User = q.User(i.Id,
            o => new
            {
                o.FirstName,
                o.LastName,
                Role = o.Role(role => role.Name)
            })
    });

Console.WriteLine($"GraphQL: {response.Query}"); // GraphQL: query GetUserWithRole($id: Int!) { me { firstName }  user(id: $id) { firstName lastName role { name }  } }
Console.WriteLine($"Me: {response.Data.MyFirstName}, User: {response.Data.User.FirstName} {response.Data.User.LastName}, Role: {response.Data.User.Role}"); // Me: Jon, User: Jon Smith, Role: Admin
Enter fullscreen mode Exit fullscreen mode

执行变更:

var response = await client.Mutation(m => m.AddUser("Jon", "Doe", o => o.Id));

Console.WriteLine($"GraphQL: {response.Query}");
Console.WriteLine($"Id: {response.Data}");
Enter fullscreen mode Exit fullscreen mode

局限性

最大的限制在于该库只能在 .NET 6 环境下运行。这是由于CallerArgumentExpression我之前提到的某个特性导致的。或许有办法解决这个问题,但我还没有花太多时间研究。
此外,该库仍处于早期开发阶段,一些功能尚未实现。
例如:

  • 片段
  • @defer属性
  • @stream 属性
  • 订阅

我认为其中一些非常重要,比如一些片段。同时,我也想分享一下我的成果并获得一些反馈。这肯定有助于指导未来的发展。

链接

Github

NuGet

文章来源:https://dev.to/byme8/zeroql-c-friend-graphql-4134