如何使用 GraphQL .Net Core、C# 和 VS Code 构建无服务器 API

2025-05-24

如何使用 GraphQL .Net Core、C# 和 VS Code 构建无服务器 API

在Twitter上关注我,很高兴接受您对主题或改进的建议/Chris

本文将指导您如何使用 GraphQL 构建完整的 CRUD API。我们最终将其托管在一个无服务器函数中,并展示如何通过外部 HTTP 调用来构建您的 API。数据存放在此处还是其他位置并不重要。GraphQL 是与用户交互的前端。

长话短说:这篇文章可能有点长,但它确实讲解了很多关于 GraphQL、查询和突变的知识。它还教你如何在 .Net Core、C# 和 VS Code 环境中创建无服务器函数。

在本文中,我们将:

  • 了解如何构建完整的 CRUD GraphQL,包括查询和突变
  • 创建一个无服务器函数并在函数中托管我们的 GraphQL API
  • 展示如何进行外部 HTTP 调用并将其作为 GraphQL API 的一部分

资源

 创建无服务器函数应用程序

我们要做的第一件事是创建一个无服务器函数。但是,什么是无服务器?为什么它在 GraphQL 环境中会很有用?这实际上是两个问题,但让我们尝试按顺序回答它们。

为什么是无服务器?

无服务器并非意味着没有服务器,而是意味着服务器不再在你的地下室里。简而言之,服务器已经迁移到云端。然而,这还不是全部。无服务器还意味着其他事情,即一切都为你设置好了。这意味着你不需要考虑你的代码在哪个操作系统上运行,也不需要考虑在哪个 Web 服务器上运行,一切都是托管的。还有更多。

总是有更多,不是吗?现在还有什么花哨的贴纸?

这和成本有关。无服务器通常意味着便宜。

为什么便宜?

嗯,无服务器代码很少运行,你只需要为它执行的时间付费

好的,你如何赚钱?

嗯,函数并不总是存在的。当有人/某物查询你的函数时,所需的资源才会被分配。

这是否会使它变得有点慢,就像在争先恐后地创建然后响应查询时可能需要一些初始等待?

你说得对。这就是所谓的冷启动。不过,我们可以通过定期轮询我们的函数或使用缩短冷启动时间的高级服务来避免这种情况

好的,那么我可以选择 100% 可用或便宜,选一个 :)

大概吧。

为什么使用 GraphQL 实现无服务器?

好的,我们对无服务器是什么以及为什么有了一些了解,那么为什么要添加 GraphQL 呢?

GraphQL 能够将来自不同 API 的数据拼接在一起,因此它可以充当一个聚合不同来源数据的 API。从某种意义上说,它在无服务器环境下运行良好,因为如果是其他人的 API,你不需要在函数中存储任何数据。

好的,听起来不错,我想没有其他好的理由了,剩下的只是关于 GraphQL 和 Serverless 的炒作,对吧?;)

...

先决条件

要创建无服务器函数,首先需要一个函数应用来放置它。为了尽可能简化操作,我们将使用 VS Code 和 Azure 函数扩展。因此,我们的先决条件是:

  1. Node.js
  2. Visual Studio 代码
  3. Azure Functions 扩展

我们可以从以下页面下载Node.js:

https://nodejs.org/en/

您可以在这里找到 Visual Studio 代码:

https://code.visualstudio.com/download

至于我们需要的扩展程序。搜索名为 的扩展程序Azure Functions。它应该如下所示:

脚手架

好了,现在我们应该一切就绪,可以创建我们的第一个无服务器函数了。如前所述,该函数需要位于一个叫做“函数应用”的东西中。所以我们需要先创建一个函数应用。

点击CMD+SHIFT+P或选择View/Command Palette调出命令面板:

现在我们需要输入一个命令来帮助我们创建一个无服务器函数应用。它叫做Azure Functions: Create New Project

然后它会询问你创建应用程序的目录。你现在所在的目录是默认的,请选择它。

接下来询问的是语言,请选择C#

接下来,它会询问您项目的第一个功能以及应如何触发它。选择HttpTrigger

现在你需要提供一个函数名。命名为 Graphql。

Function下一个问题是命名空间。你可以在这里选择任何名称,但为了方便起见,我们先这样称呼它。

最后,它会询问Access Right。这里有不同的选项Anonymous,,,FunctionAdmin我们选择Anonymous,因为我们想让我们的函数公开可用。其他选项意味着我们需要在调用函数时提供某种密钥。

VS Code 现在应该已经搭建好了所有需要的文件。它还会询问你restore所有依赖项,并下载库。你也可以运行

dotnet restore

如果您错过单击此对话框。

您的项目结构现在应如下所示:

测试一下

让我们确保新搭建的函数能够运行。首先,系统应该会提示您是否要添加调试所需的文件。您应该在这里回答。这将生成如下所示的YES目录:.vscode

包含tasks.json构建、清理和运行项目所需的任务。这些任务将帮助您完成下一步Debugging

我们从菜单中选择“调试” Debug/Start Debugging。它应该会编译代码,完成后应该如下所示:

它告诉我们去http://localhost:7071/api/Graphql

让我们启动一个浏览器:

看起来运行正常。:)

它正在命中我们的函数Graphql。说到这,让我们快速浏览一下我们在搭建 Serverless 应用时得到的示例代码:

// Graphql.cs

using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;

namespace Function
{
    public static class Graphql
    {
        [FunctionName("Graphql")]
        public static async Task<IActionResult> Run(
            [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequest req,
            ILogger log)
        {
            log.LogInformation("C# HTTP trigger function processed a request.");

            string name = req.Query["name"];

            string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
            dynamic data = JsonConvert.DeserializeObject(requestBody);
            name = name ?? data?.name;

            return name != null
                ? (ActionResult)new OkObjectResult($"Hello, {name}")
                : new BadRequestObjectResult("Please pass a name on the query string or in the request body");
        }
    }
}

现在我们不要花太多时间去理解所有内容,但可以说它完成了它的工作并且能够使用查询参数:

string name = req.Query["name"];

如果您通过 POST 发出请求,则为 Body:

string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
dynamic data = JsonConvert.DeserializeObject(requestBody);
name = name ?? data?.name;

接下来我们来谈谈 GraphQL。

 将 GraphQL 添加到无服务器

好的,我们确定要将 GraphQL 添加到我们的无服务器函数中。首先,让我们构建一个 GraphQL API。任何 GraphQL 都包含相同的移动部分:

  • Schema,它将定义我们可以查询的内容以及我们拥有的自定义数据类型
  • 解析器,一组能够响应请求并最终提供响应的函数

 添加 GraphQL 架构

这将使用 GQL( GraphQL查询语言编写。我们架构如下所示:

type Jedi {
  id: ID
  name: String,
  side: String
}

input JediInput {
  name: String
  side: String
  id: ID
}

type Mutation {
  addJedi(input: JediInput): Jedi
  updateJedi(input: JediInput ): Jedi
  removeJedi(id: ID): String
}

type Query {
    jedis: [Jedi]
    jedi(id: ID): Jedi
    hello: String
}

内容已经够多了。让我们解释一下我们在看什么。

询问

任何类型为Query或 的Mutation变量,我们都可以在我们的 API 查询中获取。 的语义表示Query你想要获取数据。在本例中,我们想要使用如下查询语法来获取 Jedis 列表:

{
  jedis { name side }
}

这相当于在 SQL 中编写以下内容:

SELECT name, side
FROM jedis;

我们做的另一件事是支持带参数的查询,即jedi(id: ID): Jedi。我们这样称呼它:

{ 
  jedi(id: 1) { name }
}

这相当于在 SQL 中编写以下内容:

SELECT name
FROM jedi
WHERE id=1;

自定义类型

所有可查询的内容都定义在 下type Query。除了 之外的所有内容都是type Mutation我们定义的自定义类型。例如:

type Jedi {
  id: ID
  name: String,
  side: String
}

突变

从语义上讲,这意味着我们将尝试更改数据。查看我们支持的操作:

addJedi(input: JediInput): Jedi
updateJedi(input: JediInput ): Jedi
removeJedi(id: ID): String

可以看到我们支持添加、更新、删除。

要调用突变,我们需要输入如下内容:

mutation test { 
  addJedi(input: { 
    name: "JarJar", 
    side: "Dark"  
  }) { name } 
}

输入,用于变异的复杂输入

input注意中的输入属性addJedi()。如果我们查看模式,我们可以看到它的类型JediInput定义如下:

input JediInput {
  name: String
  side: String
  id: ID
}

我们之所以使用关键字input而不是type是因为这是一个特殊情况。你想知道它到底有多特殊吗?其实,突变有两种类型的输入参数:

  1. 标量,例如字符串、ID、布尔值等,也称为原语
  2. 输入,这只不过是一个具有许多属性的复杂数据类型,例如JediInput

因此绝对可以定义如下所示的突变:

type Mutation {
  addTodo(todo: String!): String
}

所以价值百万美元的问题是为什么我不能只使用自定义类型Jedi作为我的突变的输入参数类型?

诚实的回答是我不知道。

只要记住这一点:如果您需要一个比标量更复杂的输入参数,那么您需要像这样定义它:

input MyInputType {
  // my columns
}

添加 NuGet 包

好的,下一步是在代码中正确设置此模式。为此,我们将创建一个文件Server.cs并安装 GraphQL 包,如下所示:

dotnet add package GraphQL 

创建架构

现在将以下代码添加到Server.cs,如下所示:

using GraphQL;
using GraphQL.Types;
using Newtonsoft.Json;
using System.Threading.Tasks;

namespace Function {
  public class Server 
  {
    private ISchema schema { get; set; }
    public Server() 
    {
      this.schema = Schema.For(@"
          type Jedi {
            id: ID
            name: String,
            side: String
          }

          input JediInput {
            name: String
            side: String
            id: ID
          }

          type Mutation {
            addJedi(input: JediInput): Jedi
            updateJedi(input: JediInput ): Jedi
            removeJedi(id: ID): String
          }

          type Query {
              jedis: [Jedi]
              jedi(id: ID): Jedi
          }
      ", _ =>
      {
        _.Types.Include<Query>();
        _.Types.Include<Mutation>();
      });

    }

    public async Task<string> QueryAsync(string query) 
    {
      var result = await new DocumentExecuter().ExecuteAsync(_ =>
      {
        _.Schema = schema;
        _.Query = query;
      });

      if(result.Errors != null) {
        return result.Errors[0].Message;
      } else {
        return JsonConvert.SerializeObject(result.Data);
      }
    }
  }
}

在上面的构造函数中,我们通过调用一个表示我们模式的字符串(以 GQL 语言表示)来设置模式Schema.For()。但是,第二个参数无法编译,即以下部分:

_ =>
  {
    _.Types.Include<Query>();
    _.Types.Include<Mutation>();
  }

添加解析器

它无法编译的原因是QueryMutation还不存在。它们只是响应查询和变更请求的解析器​​类。让我们先创建Db.cs一个文件,让它编译通过。Query.cs

添加内存数据库

// Db.cs

using System.Collections.Generic;
using System.Linq;

namespace Function
{
  public class StarWarsDB
  {
    private static List<Jedi> jedis = new List<Jedi>() {
      new Jedi(){ Id = 1, Name ="Luke", Side="Light"},
      new Jedi(){ Id = 2, Name ="Yoda", Side="Light"},
      new Jedi(){ Id = 3, Name ="Darth Vader", Side="Dark"}
    };
    public static IEnumerable<Jedi> GetJedis()
    {
      return jedis;
    }

    public static Jedi AddJedi(Jedi jedi)
    {
      jedi.Id = jedis.Count + 1;
      jedis.Add(jedi);
      return jedi;
    }

    public static Jedi UpdateJedi(Jedi jedi)
    {
      var toUpdate = jedis.SingleOrDefault(j => j.Id == jedi.Id);
      toUpdate.Name = jedi.Name;
      toUpdate.Side = jedi.Side;
      return toUpdate;
    }

    public static string RemoveJedi(int id)
    {
      var toRemove = jedis.SingleOrDefault(j => j.Id == id);
      jedis.Remove(toRemove);
      return "success";
    }
  }

  public class Jedi
  {
    public int Id { get; set; }
    public string Name { get; set; }
    public string Side { get; set; }
  }
}

Db.cs只不过是一个简单的内存数据库。

添加解析器类来处理所有查询

接下来,让我们创建Query.cs

// Query.cs

using System.Collections.Generic;
using GraphQL;
using System.Linq;

namespace Function
{
  public class Query
  {
    [GraphQLMetadata("jedis")]
    public IEnumerable<Jedi> GetJedis()
    {
      return StarWarsDB.GetJedis();
    }

    [GraphQLMetadata("jedi")]
    public Jedi GetJedi(int id)
    {
      return StarWarsDB.GetJedis().SingleOrDefault(j => j.Id == id);
    }

    [GraphQLMetadata("hello")]
    public string GetHello()
    {
      return "World";
    }
  }

}

上面代码的有趣之处在于我们如何将 GraphQL 模式中的某些内容映射到解析器函数。为此,我们使用了GraphQLMetadata如下装饰器:

[GraphQLMetadata("jedis")]
public IEnumerable<Jedi> GetJedis()
{
  return StarWarsDB.GetJedis();
}

上面告诉我们,如果用户查询,jedis那么该函数GetJedis()就会响应。

处理参数几乎同样简单。还是同样的装饰器,但我们只需像这样添加输入参数:

[GraphQLMetadata("jedi")]
public Jedi GetJedi(int id)
{
  return StarWarsDB.GetJedis().SingleOrDefault(j => j.Id == id);
}

添加解析器类来处理所有 Mutation 请求

我们快完成了,但我们需要类Mutation.cs,让我们向其中添加以下内容:

// Mutation.cs

using GraphQL;

namespace Function {
  public class Mutation
  {
    [GraphQLMetadata("addJedi")]
    public Jedi AddJedi(Jedi  input) 
    {
      return StarWarsDB.AddJedi(input);
    }

    [GraphQLMetadata("updateJedi")]
    public Jedi UpdateJedi(Jedi input)
    {
      return StarWarsDB.AddJedi(input);
    }

    [GraphQLMetadata("removeJedi")]
    public string AddJedi(int id)
    {
      return StarWarsDB.RemoveJedi(id);
    }
  }
}

正如您所见,它看起来非常相似Query.cs,甚至带有参数处理。

更新我们的无服务器函数

现在只剩下一块拼图了,那就是我们的无服务器函数。我们需要对其进行修改,以便支持用户像这样调用我们的函数:

url?query={ jedis } { name }

将代码更改为Graphql.cs以下内容:


using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;

namespace Function
{
    public static class GraphQL
    {
        [FunctionName("GraphQL")]
        public static async Task<IActionResult> Run(
            [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequest req,
            ILogger log)
        {
            var server = new Server();
            string query = req.Query["query"];
            // string query = "mutation test { addJedi(input: { name: \"JarJar\", side: \"Dark\"  }) { name } }";

            var json = await server.QueryAsync(query);
            return new OkObjectResult(json);
        }
    }
}


测试一切

为了测试它,我们只需从菜单中选择Debug/Start Debugging并更改浏览器中的 URL 即可开始调试:

http://localhost:7071/api/GraphQL?query={ jedis { name } }

让我们看看浏览器说了什么:

是的,我们做到了。使用 GraphQL API 的无服务器函数。

奖励 - 调用其他端点

现在。GraphQL 的一大优点是它允许我们调用其他 API,从而将 GraphQL 充当聚合层。我们可以通过以下步骤轻松实现这一点:

  1. 执行 HTTP 请求,这应该向我们的外部 API 发出请求
  2. 为我们的外部调用添加解析器方法
  3. 使用新类型更新我们的架构
  4. 测试一下

HTTP 请求

我们可以轻松地在 .Net 中发出 HTTP 请求,因此HttpClient让我们创建一个Fetch.cs如下类:

// Fetch.cs

using System.Net.Http;
using System.Threading.Tasks;

namespace Function {
  public class Fetch
  {
    private static string BaseUrl = "https://swapi.co/api";
    public static async Task<string> ByUrl(string url)
    {
      using (var client = new HttpClient())
      {
        var json = await client.GetStringAsync(string.Format("{0}/{1}", BaseUrl, url));

        return json;
      }
    }
  }
}

添加解析器方法

现在打开Query.cs并将以下方法添加到Query类中:

[GraphQLMetadata("planets")]
  public async Task<List<Planet>> GetPlanet() 
  {
    var planets = await Fetch.ByUrl("planets/");
    var result = JsonConvert.DeserializeObject<Result<List<Planet>>>(planets);
    return result.results;
  }

此外,我们应该安装一个新的 NuGet 包:

dotnet add package Newtonsoft.Json

这是必需的,这样我们就可以将得到的 JSON 响应转换为 Poco。

我们还应该将类型Planet和添加ResultQuery.cs,但在类定义之外:

public class Result<T>
{
  public int count { get; set; }
  public T results { get; set; }
}

public class Planet
{
  public string name { get; set; }
}

使用新类型更新我们的架构

在尝试之前,我们还需要更新一下架构。Schema.cs现在让我们打开它,确保它看起来像这样:

this.schema = Schema.For(@"
          type Planet {
            name: String
          }

          type Jedi {
            id: ID
            name: String,
            side: String
          }

          input JediInput {
            name: String
            side: String
            id: ID
          }

          type Mutation {
            addJedi(input: JediInput): Jedi
            updateJedi(input: JediInput ): Jedi
            removeJedi(id: ID): String
          }

          type Query {
              jedis: [Jedi]
              jedi(id: ID): Jedi
              hello: String
              planets: [Planet]
          }
      ", _ =>
      {
        _.Types.Include<Query>();
        _.Types.Include<Mutation>();
      });

上面我们添加了类型Planet

type Planet {
  name: String
}

我们还添加planetsQuery

planets: [Planet]

测试一下

就是这样,让我们​​测试一下。Debug/Start Debugging

http://localhost:7071/api/GraphQL?query={ planets { name } }

就是这样。:)

我们成功地实现了对 API 的外部调用,并将其作为 GraphQL API 的一部分。如您所见,我们可以轻松地混合来自不同来源的数据。

概括

我们已经成功创建了一个支持完整 CRUD 的 GraphQL。也就是说,我们不仅支持查询数据,还支持创建、更新和删除数据。

此外,我们已经迈出了无服务器功能的第一步。

最后,我们添加了 GraphQL API 以供无服务器功能提供服务。

额外的好处是,我们还展示了如何利用外部 API 并将其融入我们的 API。这真的很强大。

希望你喜欢这篇文章。下一部分我们将探讨如何将 GraphQL 添加到 .Net Core 中的 Web API 项目。

文章来源:https://dev.to/azure/how-you-can-build-a-serverless-api-using-graphql-net-core-c-and-vs-code-g5h
PREV
如何从零开始学习 Docker。涵盖 Docker-compose、卷、数据库、云等内容
NEXT
我如何撰写在线文章