如何使用 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 的一部分
资源
-
.Net 中的 GraphQL 入门
这是我写的第一篇关于 .Net 中 GraphQL 的文章。建议你先看一看,了解一下 GraphQL 的基础知识。 -
从 Azure Functions 开始,
涵盖创建无服务器函数并使用 VS Code 部署到云 -
从 .Net 中的无服务器开始
.Net C# 开发人员参考 Azure Functions -
使用 C# 和 .Net 创建无服务器 API
我撰写的文章介绍了如何使用无服务器、.Net、C# 和 VS Code 创建 CRUD API -
创建您的第一个无服务器函数
学习模块,用于创建 Azure 函数 -
免费 Azure 帐户
要部署无服务器 Azure 函数,您需要一个免费的 Azure 帐户)
创建无服务器函数应用程序
我们要做的第一件事是创建一个无服务器函数。但是,什么是无服务器?为什么它在 GraphQL 环境中会很有用?这实际上是两个问题,但让我们尝试按顺序回答它们。
为什么是无服务器?
无服务器并非意味着没有服务器,而是意味着服务器不再在你的地下室里。简而言之,服务器已经迁移到云端。然而,这还不是全部。无服务器还意味着其他事情,即一切都为你设置好了。这意味着你不需要考虑你的代码在哪个操作系统上运行,也不需要考虑在哪个 Web 服务器上运行,一切都是托管的。还有更多。
总是有更多,不是吗?现在还有什么花哨的贴纸?
这和成本有关。无服务器通常意味着便宜。
为什么便宜?
嗯,无服务器代码很少运行,你只需要为它执行的时间付费
好的,你如何赚钱?
嗯,函数并不总是存在的。当有人/某物查询你的函数时,所需的资源才会被分配。
这是否会使它变得有点慢,就像在争先恐后地创建然后响应查询时可能需要一些初始等待?
你说得对。这就是所谓的冷启动。不过,我们可以通过定期轮询我们的函数或使用缩短冷启动时间的高级服务来避免这种情况。
好的,那么我可以选择 100% 可用或便宜,选一个 :)
大概吧。
为什么使用 GraphQL 实现无服务器?
好的,我们对无服务器是什么以及为什么有了一些了解,那么为什么要添加 GraphQL 呢?
GraphQL 能够将来自不同 API 的数据拼接在一起,因此它可以充当一个聚合不同来源数据的 API。从某种意义上说,它在无服务器环境下运行良好,因为如果是其他人的 API,你不需要在函数中存储任何数据。
好的,听起来不错,我想没有其他好的理由了,剩下的只是关于 GraphQL 和 Serverless 的炒作,对吧?;)
...
先决条件
要创建无服务器函数,首先需要一个函数应用来放置它。为了尽可能简化操作,我们将使用 VS Code 和 Azure 函数扩展。因此,我们的先决条件是:
- Node.js
- Visual Studio 代码
- Azure Functions 扩展
我们可以从以下页面下载Node.js:
您可以在这里找到 Visual Studio 代码:
至于我们需要的扩展程序。搜索名为 的扩展程序Azure Functions
。它应该如下所示:
脚手架
好了,现在我们应该一切就绪,可以创建我们的第一个无服务器函数了。如前所述,该函数需要位于一个叫做“函数应用”的东西中。所以我们需要先创建一个函数应用。
点击CMD+SHIFT+P
或选择View/Command Palette
调出命令面板:
现在我们需要输入一个命令来帮助我们创建一个无服务器函数应用。它叫做Azure Functions: Create New Project
然后它会询问你创建应用程序的目录。你现在所在的目录是默认的,请选择它。
接下来询问的是语言,请选择C#
。
接下来,它会询问您项目的第一个功能以及应如何触发它。选择HttpTrigger
。
现在你需要提供一个函数名。命名为 Graphql。
Function
下一个问题是命名空间。你可以在这里选择任何名称,但为了方便起见,我们先这样称呼它。
最后,它会询问Access Right
。这里有不同的选项Anonymous
,,,Function
。Admin
我们选择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
是因为这是一个特殊情况。你想知道它到底有多特殊吗?其实,突变有两种类型的输入参数:
- 标量,例如字符串、ID、布尔值等,也称为原语
- 输入,这只不过是一个具有许多属性的复杂数据类型,例如
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>();
}
添加解析器
它无法编译的原因是Query
和Mutation
还不存在。它们只是响应查询和变更请求的解析器类。让我们先创建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 充当聚合层。我们可以通过以下步骤轻松实现这一点:
- 执行 HTTP 请求,这应该向我们的外部 API 发出请求
- 为我们的外部调用添加解析器方法
- 使用新类型更新我们的架构
- 测试一下
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
和添加Result
到Query.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
}
我们还添加planets
了Query
:
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