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 });
它等价于以下 GraphQL 查询:
query { user(id: 42) { id firstName lastName } }
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!
}
初始设置
现在,我们来创建一个可以访问它的控制台应用程序。我们可以使用以下命令来实现:
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
最后一步可以放在 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>
乍一看可能有点复杂,但别担心,我们只需要做一次。
初始设置已完成,我们可以执行第一个查询了。让我们修改 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
如您所见,工作流程非常简单。创建 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);
}
这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 }));
这是确保所有输入都被分析的最简单方法。通过这种方法,我们可以将它们作为参数,并将它们序列化以添加到请求中。
支持的功能
现在让我们来看看目前支持哪些功能。
例如,我们可以获取深度嵌套字段:
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
此外,还可以同时触摸多个字段:
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
执行变更:
var response = await client.Mutation(m => m.AddUser("Jon", "Doe", o => o.Id));
Console.WriteLine($"GraphQL: {response.Query}");
Console.WriteLine($"Id: {response.Data}");
局限性
最大的限制在于该库只能在 .NET 6 环境下运行。这是由于CallerArgumentExpression我之前提到的某个特性导致的。或许有办法解决这个问题,但我还没有花太多时间研究。
此外,该库仍处于早期开发阶段,一些功能尚未实现。
例如:
- 片段
- @defer属性
- @stream 属性
- 订阅
我认为其中一些非常重要,比如一些片段。同时,我也想分享一下我的成果并获得一些反馈。这肯定有助于指导未来的发展。