65.9K
CodeProject 正在变化。 阅读更多。
Home

HotChocolate 上的更多高级 GraphQL 概念

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.21/5 (4投票s)

2024 年 3 月 7 日

CPOL

8分钟阅读

viewsIcon

3674

downloadIcon

60

使用 HotChocolate 深入了解 GraphQL 的更高级概念

引言

在之前的一系列文章中,我们广泛探讨了 GraphQL 的本质及其在 2010 年代初由 Facebook 提出的原因。具体来说,GraphQL 使我们能够精确地检索所需数据,不多也不少,同时避免了大量端点的需求。有关详细说明,请参阅此处:使用 HotChocolate 构建 GraphQL API

虽然我们简要演示了一个使用 HotChocolate 和 JavaScript 客户端的示例,展示了 GraphQL 只提供所请求有效负载的能力,但我们没有深入探讨 GraphQL 的高级复杂性。因此,本系列的重点是通过深入研究规范中最复杂的部分来弥补这一差距。我们的目标是全面阐述最复杂的方面。

以下书籍对于完成本系列很有帮助

本文最初发布于此

什么是 GraphQL?

GraphQL 起源于 Facebook 在 2010 年初的开发工作,主要目标是优化网络数据传输,特别是对于带宽效率至关重要的移动应用程序。随后,它被正式化为服务器必须遵守的**规范**,以满足合同要求。这种灵活的方法允许 GraphQL 服务器在几乎任何编程语言中实现,并且在同一种语言中发现多种实现并不罕见。

非常重要

必须强调的是,GraphQL 本身只是一项**规范**。因此,利用此技术的 API 需要部署在符合此规范并能够理解和处理以相应语言编写的请求的服务器上。实际上,GraphQL 服务器通常通过安装为 GraphQL 实现量身定制的库或运行时环境来实例化。

HotChocolate 是 GraphQL 规范的 C# 实现的一个示例。

有关更多详细信息,请参阅

什么是 HotChocolate?

HotChocolate 是 Microsoft .NET 平台的开源 GraphQL 服务器,符合最新的 GraphQL 2021 年 10 月规范 + 草案,这使得 Hot Chocolate 与所有符合 GraphQL 的客户端兼容。
https://chillicream.com/docs/hotchocolate/v13

我们将使用 HotChocolate 在 Azure Function 中设置一个基本的 GraphQL 服务器,然后使用 JavaScript 查询它。

创建环境

  • 创建一个名为 EOCS.GraphQLAdvanced 的新解决方案(例如),并在其中创建一个新的 Azure Functions 项目。

  • 创建一个名为 Customer.cs 的新类,并在此处添加以下代码

    public class Customer
    {
        public string Id { get; set; }
    
        public string Name { get; set; }
    
        public int Age { get; set; }
    }
  • 创建一个名为 ICustomerRepository 的新接口,并在此处添加以下代码
    public interface ICustomerRepository
    {
        List<Customer> GetAllCustomers();
        Customer GetCustomerById(string id);
    }
  • 创建一个名为 MockCustomerRepository 的新类,实现 ICustomerRepository 接口。
    public class MockCustomerRepository : ICustomerRepository
    {
        public List<Customer> GetAllCustomers()
        {
            return new List<Customer>()
            {
                new Customer(){ Id = "0001", Name = "Bruce Smith", Age = 45 },
                new Customer(){ Id = "0010", Name = "Melissa Price", Age = 52 }
            };
        }
        
        public Customer GetCustomerById(string id)
        {
            return new Customer(){ Id = "0001", Name = "Bruce Smith", Age = 45 };
        }
    }
  • 添加 HotChocolate.AzureFunctions NuGet 包

  • 创建一个名为 CustomerService.cs 的类(或更通用的名称),并在此处添加以下代码

    public class CustomerService
    {
        private readonly IGraphQLRequestExecutor _executor;
    
        public CustomerService(IGraphQLRequestExecutor executor)
        {
            _executor = executor;
        }
    
        [FunctionName(nameof(Run))]
        public async Task<IActionResult> Run([HttpTrigger(AuthorizationLevel.Anonymous, 
                     "get", "post", Route = "graphql/{**slug}")] HttpRequest req)
        {
            return await _executor.ExecuteAsync(req);
        }
    }
  • 创建一个名为 StartUp.cs 的类来引导 Azure Function。
    public class StartUp : FunctionsStartup
    {
        public override void Configure(IFunctionsHostBuilder builder)
        {
            ConfigureServices(builder.Services);
        }
    
        private static void ConfigureServices(IServiceCollection services)
        {
            services.AddSingleton<ICustomerRepository, MockCustomerRepository>();
        }
    }

非常重要

此设置目前无法正常运行;它仅仅为未来的开发奠定了基础。

使用服务

现在是时候使用我们的服务了,为此我们将创建一个简单的 HTML 文件。

  • 创建一个新的 ASP.NET Core Web App 项目,例如命名为 EOCS.GraphQLAdvanced.Client,并在 wwwroot 中添加一个名为 indexQuery.html 的文件。

  • 在此处添加以下代码

    <html>
    <head>
        <title>GraphQL</title>
    </head>
    <body>
        <pre><code class="language-json" id="code"></code></pre>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/js-beautify/1.14.9/beautify.min.js"></script>
        
    </body>
    </html>
  • 编辑 Program.cs 代码
    public class Program
    {
        public static void Main(string[] args)
        {
            var builder = WebApplication.CreateBuilder(args);
            var app = builder.Build();
            
            app.UseDefaultFiles();
            
            app.UseStaticFiles();
            
            app.Run(async (context) =>
            {
                await context.Response.WriteAsync
                      ("Request Handled and Response Generated");
            });
            
            app.Run();
        }
    }

重要

请确保在启动时设置多个项目。

另外,请确保通过修改 host.json 文件来授权 CORS

{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "UseDevelopmentStorage=true",
    "FUNCTIONS_WORKER_RUNTIME": "dotnet"
  },
  "Host": {
    "CORS": "*"
  }
}

信息

我们的基础代码相当简单,主要包括一个请求,用于检索 customers 列表或单个 customer。虽然看起来并不起眼,但这种基本设置是演示更高级概念的合适起点。

必须再次强调:GraphQL 作为一项规范运行,需要在服务器端安装或实现运行时。因此,客户端不能仅仅被动等待数据;相反,它承担了积极的角色,并且必须明确地阐述其数据需求。本节将阐明这一过程在 GraphQL 规范中的定义方式,详细说明客户端如何指定其请求。

SDL 是什么意思?

GraphQL 规范引入了其独特的类型语言,称为 Schema Definition Language (SDL),它是用于创建 GraphQL schema 的媒介。例如,一个客户在 SDL 中将定义如下。

type Customer {
   id: ID!
   name: String
   age: Int
}

在此代码片段中,我们定义了一个 Customer 类型,包含三个字段:一个 ID 类型,一个代表字母数字数据的 String 类型,以及一个表示整数的 Int 类型。值得注意的是,我们通过在其后附加感叹号 (!) 来指定 Id 字段是强制性的。

信息

ID 类型解析为 string,但需要一个唯一值。

类似地,我们可以定义一个 Order 类型来表示电子商务应用中的订单。它可能定义如下

type Order {
   id: ID!
   reference: String!
   amount: Float
   customerId: ID!
}

然而,orderscustomers 并不是互斥的实体;customers 可以有多个订单,而一个 order 通常与一个特定的 customer 相关联。因此,我们可以增强模型,如下所示

type Customer {
   id: ID!
   name: String
   age: Int
   orders: [Order]
}

type Order {
   id: ID!
   reference: String!
   amount: Float
   customer: Customer
}

在这里,我们指定一个 customer 可以拥有一个订单数组,用方括号 [] 表示。

如何对这些类型执行查询?

CustomerOrder 类型本身不为客户端应用程序提供任何功能;它们仅勾勒出可用实体的结构。要执行查询,GraphQL 规范要求包含 Query 根类型。

信息

根类型是什么意思?它们是规范中概述的固有类型,可以被视为任何 GraphQL API 的入口点。本质上,有三种不同的根类型可用

  1. Query(用于数据检索)
  2. Mutation(用于数据修改),以及
  3. 订阅

为了使客户端能够检索所有 customers 或通过 ID 检索特定 customer,我们必须定义一个 Query 类型,如下所示。

type Query {
   customers: Customer
   customer(id:ID!): Customer
}

此时,服务器已完成其任务,客户端承担责任。客户端不再可以简单地访问一个端点并被动等待响应;它必须主动指定其所需数据。这可以通过 GETPOST 请求来完成。

The client assumes responsibility.

客户端承担责任。

信息

如我们先前系列中所述,无需大量端点。实际上,一个单一的端点足以满足需求,通常表示为 /graphql。我们将相应地遵循此指导。

使用 POST 请求进行查询

标准的 GraphQL POST 请求应使用 application/json 内容类型,并包含以下形式的 JSON 编码体。

POST http://<path_api>/graphql
{
    "query": "query customer (id: "123f0") { id, age, orders { reference } }",
}

根据 GraphQL 规范,响应将由格式化的 JSON 有效负载组成。

{
  "data": {
    "customer": {
      "id": "123f0",
      "age": "45",
	  "orders": []
    }
  }
}

信息

如果响应包含错误,GraphQL 将提供相应的格式化消息。

{
  "errors": [
    {
      "message": "<...>",
      "locations": [
        {
          "line": 2,
          "column": 1
        }
      ]
    }
  ]
}

使用 GET 请求进行查询

执行 GET 请求也是可行的,但它往往更麻烦;因此,与 POST 请求相比,它的使用较少。对于感兴趣的读者,我们建议参考官方文档以获取更多详细信息。

什么是解析器?

到目前为止,我们仅在 SDL 中概述了可用的类型和查询。但是,我们还没有解决如何从任何数据存储中检索字段。**解析器就是这样**:它们充当规范(定义我们想要的结构)和具体实现(详细说明如何获取它)之间的桥梁。

信息

GraphQL 规范没有提供关于如何实现解析器的确切指南。它主要强调此概念必须存在于库中并且是可扩展的。

我们不会深入探讨这个概念,因为它对大多数开发人员来说似乎是直观的:最终,API 要想正常运行,必须在某个时候从数据存储中收集数据。**解析器只是一个理解如何提取某些数据的函数。**

现在我们已经理解了规范中的查询和解析器的概念,让我们探讨一下它在实践中是如何具体实现的。

SDL 如何与 HotChocolate 集成?

信息

此实现完全特定于 HotChocolate,其他库可能以不同的方式实现查询和解析器。

实现解析器

回想上一篇文章,我们使用了 MockCustomerRepository 类来与 customers 交互。

public class MockCustomerRepository : ICustomerRepository
{
    public List<Customer> GetAllCustomers()
    {
        return new List<Customer>()
        {
            new Customer(){ Id = "0001", Name = "Bruce Smith", Age = 45 },
            new Customer(){ Id = "0010", Name = "Melissa Price", Age = 52 }
        };
    }
    
    public Customer GetCustomerById(string id)
    {
        return new Customer(){ Id = "0001", Name = "Bruce Smith", Age = 45 };
    }
}

如果到目前为止一切都已理解,那么这些方法将作为解析器,并且需要在某个时候调用它们来检索数据。

实现 Query 类型

在探索如何使用查询检索数据之前,我们需要了解 SDL 类型如何在 C# 中表示。HotChocolate 只需要创建简单的 POCO(普通 CLR 对象)类,如下所示

public class Customer
{
    public string Id { get; set; }

    public string Name { get; set; }

    public int Age { get; set; }
    
    public List<Order> Orders { get; set; }
}

public class Order
{
    public string Id { get; set; }

    public string Reference { get; set; }

    public decimal Amount { get; set; }
    
    public Customer Customer { get; set; }
}

有了这些类之后,我们就可以执行查询了。为此,HotChocolate 要求定义一个 Query 类并将所有组件(POCO 类和解析器)集成在一起。

public class Query
{
    public List<Customer> GetCustomers([Service] ICustomerRepository customerRepository)
    {
        return customerRepository.GetAllCustomers();
    }
    
    public Customer GetCustomerById
           ([Service] ICustomerRepository customerRepository, string id)
    {
        return customerRepository.GetCustomerById(id);
    }
}

在这里,我们观察到解析器通过依赖注入使用 [Service] 注释获得。但是,值得注意的是,这并不是管理解析器的唯一方法,也不是最常见的方法。有关更多详细信息,请参阅文档。

配置完所有组件后,我们需要修改 StartUp 类,指示服务器遵守 GraphQL 规范。

public class StartUp : FunctionsStartup
{
    public override void Configure(IFunctionsHostBuilder builder)
    {
        ConfigureServices(builder.Services);
    }

    private static void ConfigureServices(IServiceCollection services)
    {
        services.AddSingleton<ICustomerRepository, MockCustomerRepository>();

        services.AddGraphQLFunction().AddQueryType<Query>();
    }
}

信息

此示例说明了 HotChocolate 如何通过允许我们使用原生类轻松定义 GraphQL 类型来简化我们的任务。在后台,HotChocolate 通过遵守 GraphQL 规范来处理繁重的工作。

使用服务

我们现在将转到客户端项目。

  • 编辑 indexQuery.html 文件
    <html>
    <head>
        <title>GraphQL</title>
    </head>
    <body>
        <pre><code class="language-json" id="code"></code></pre>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/js-beautify/1.14.9/beautify.min.js"></script>
        <script>
            (async function () {
                const data = JSON.stringify({
                    query: `query {
                              customerById(id:"0010") {
                                id
                                name
                                age
                              }
                            }`
                });
    
                const response = await fetch(
                    'https://:7132/api/graphql',
                    {
                        method: 'post',
                        body: data,
                        headers: {
                            'Content-Type': 'application/json'
                        },
                    }
                );
    
                const json = await response.json();
                document.getElementById('code').innerHTML = js_beautify(
                    JSON.stringify(json.data)
                );
            })();
        </script>
    </body>
    </html>

    重要

    在前面的示例中,我们注意到 JavaScript 中调用的方法名是 customerByIdHotChocolate 使用这种命名约定:服务器期望找到一个以 customerById 结尾的解析器(不区分大小写)。

  • 运行程序

    导航到 indexQuery.html 文件并观察结果。

    很明显,这些机制运行正常,并且正在调用正确的解析器。

到此为止,我们关于 Query 类型的讨论就结束了,但这仅仅是旅程的开始。接下来,我们将深入探讨如何使用 GraphQL 修改数据。但是为了避免使本文过于冗长,有兴趣了解此实现的读者可以在此处找到后续内容。

历史

  • 2024 年 3 月 7 日:初始版本
© . All rights reserved.