使用 GraphQL 的 .NET 5 API - 分步指南
由 Mux 主办的 DEV 全球展示挑战赛:展示你的项目!
本文将介绍如何使用 ASP.NET Core 5 实现 GraphQL,并构建一个待办事项列表功能。
你可以在 YouTube 上观看完整视频。
您可以通过以下链接从 GitHub 获取源代码:
https://github.com/mohamadlawand087/v28-Net5-GraphQL
那么我们今天将要讲的内容有:
- 我们想要解决的问题
- 什么是 GraphQL
- REST 与 GraphQL
- 核心概念
- 原料
- 编码
和往常一样,源代码在下方说明中。如果您喜欢这个视频,请点赞、分享和订阅。这对频道的发展非常有帮助。
问题与解决方案
为了理解 GraphQL,让我们先来了解它所解决的问题。
它最初由 Facebook 开发,旨在满足其数据获取需求。早在 2012 年 Facebook 发布该应用时,就因其性能、延迟和耗电等问题而饱受诟病,因为它需要进行大量的 API 调用来获取用户数据。
为了解决这个问题,Facebook 引入了 GraphQL,它将所有这些请求合并成一个单独的请求。有了这个单一的 API 端点,我们就可以使用 GraphQL 查询语言从服务器检索任何数据。我们只需告诉 API 我们想要的信息,API 就会返回它,这就能解决很多问题。
让我们分析一下这个查询。实际上,我们在同一个请求中嵌套了对象,并通过嵌套对象请求更多信息。我们以 JSON 格式收到结果。因此,在 REST API 中,获取这些信息可能需要多次请求,而在 GraphQL 中,只需一次调用即可。
我们正在处理原本不同的请求,并让客户端负责弄清楚处理数据所需的逻辑,以获取我们发送的信息。我们将所有这些请求委托给服务器,并要求服务器根据我们发送的 GraphQL 查询来处理信息绑定。
什么是 GraphQL
GraphQL 是一种用于 API 的查询语言,也是一个运行时环境,用于使用现有数据来执行这些查询。GraphQL 提供 API 中数据的完整且易于理解的描述,使客户端能够精确地请求所需信息,不多也不少。
一次请求即可获取多个资源: GraphQL 查询不仅可以访问单个资源的属性,还能顺畅地追踪资源之间的引用关系。传统的 REST API 需要从多个 URL 加载数据,而 GraphQL API 只需一次请求即可获取应用所需的所有数据。
GraphQL 的核心是数据树,它允许我们建立关系,并将数据聚合操作下推到服务器端。
- 一个端点
- 一项请求
- 类型系统
GraphQL 与 REST
休息
- 针对不同数据类型的多个端点
- 使用链式请求来获取我们需要的数据
- 过度获取:我们获取的信息比我们需要的还要多。
- 获取数据不足:我们获取的数据较少,因此需要发出大量请求才能获取信息。
GraphQL
- 一个端点
- 一个请求,不同的映射
- 不进行过度取样
- 没有低位取力
- 类型系统
- 可预测的
何时使用
休息
- 非交互式(系统间)
- 微服务
- 简单对象层次结构
- 重复简单查询
- 更容易开发
- 对客户而言,理解起来更加复杂。
GraphQL
- 实时应用
- 移动应用
- 复杂对象层次结构
- 复杂查询
- 开发过程复杂
- 客户更容易接受
核心概念
模式:完整描述 API,包括查询、对象、数据类型和描述。其部分属性如下:
- 自我记录
- 由多种类型组成
- 必须具有根查询类型
类型:它可以是任何东西,以下是一些类型:
- 询问
- 突变
- 订阅
- 对象
- 枚举
- 标量
解析器:返回给定字段的数据
数据来源:
- 数据来源
- 微服务
- REST API
变异:将允许我们编辑和添加数据
订阅:一种基于 WebSocket 的连接,它允许我们在执行操作后发送实时消息。
原料
VS Code(https://code.visualstudio.com/download)
.Net 5(https://dotnet.microsoft.com/download)
Insomnia(https://insomnia.rest/download)
DBeaver(https://dbeaver.io/download/)
热巧克力
是 GraphQL 的一种实现,也是在 .Net Core 中编写 GraphQL 服务器的框架。
我们需要检查.NET的版本。
dotnet --version
现在我们需要安装实体框架工具。
dotnet tool install --global dotnet-ef
完成后,我们来创建应用程序。
dotnet new web -n TodoListGQL
现在我们需要安装所需的软件包。
dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet add package Microsoft.EntityFrameworkCore.Sqlite
dotnet add package HotChocolate.AspNetCore
dotnet add package HotChocolate.Data.EntityFramework
dotnet add package GraphQL.Server.Ui.Voyager
现在让我们检查一下应用程序和源代码,构建一下应用程序,看看它是否能运行。
dotnet build
dotnet run
现在让我们开始开发,首先需要创建模型并构建 DbContext。在应用程序的根目录下创建一个名为 Models 的新文件夹,然后在 Models 文件夹内创建两个新类,分别命名为 ItemData.cs 和 ItemList.cs。
public class ItemData
{
public int Id { get; set; }
public string Title { get; set; }
public string Description { get; set; }
public bool Done { get; set; }
public int ListId { get; set; }
public virtual ItemList ItemList { get; set; }
}
public class ItemList
{
public ItemList()
{
ItemDatas = new HashSet<ItemData>();
}
public int Id { get; set; }
public string Name { get; set; }
public virtual ICollection<ItemData> ItemDatas { get; set; }
}
现在我们需要创建应用程序数据库上下文,因此在根目录下创建一个名为 Data 的新文件夹,并在 Data 文件夹内创建一个名为 ApiDbContext 的新类。
public class ApiDbContext : DbContext
{
public virtual DbSet<ItemData> Items {get;set;}
public virtual DbSet<ItemList> Lists {get;set;}
public ApiDbContext(DbContextOptions<ApiDbContext> options)
: base(options)
{ }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<ItemData>(entity =>
{
entity.HasOne(d => d.ItemList)
.WithMany(p => p.ItemDatas)
.HasForeignKey(d => d.ListId)
.OnDelete(DeleteBehavior.Restrict)
.HasConstraintName("FK_ItemData_ItemList");
});
}
}
现在我们需要更新 appsettings.json 文件以及启动类。
首先,让我们打开应用程序设置并添加以下代码。
"ConnectionStrings": {
"DefaultConnection" : "DataSource=app.db; Cache=Shared"
},
现在让我们更新启动类,将我们的应用程序连接到数据库。
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<ApiDbContext>(options =>
options.UseSqlite(
Configuration.GetConnectionString("DefaultConnection")
));
}
现在我们要构建数据库,为此我们需要使用 EF Core 迁移。
dotnet ef migrations add "Initial Migrations"
dotnet ef database update
数据库更新成功完成后,我们可以看到有一个名为 migrations 的新文件夹,其中包含负责创建数据库及其表 Item 的 C# 脚本。
我们可以通过查看根目录中的 app.db 文件来验证数据库是否已创建,我们还可以使用 SQLite 浏览器 (dbeaver) 来验证表是否已成功创建。
现在我们需要开始将 GraphQL 集成到我们的应用程序中,我们要做的第一件事是在应用程序的根目录中添加一个名为 GraphQL 的新文件夹。
现在,在 GraphQL 文件夹中,我们将创建一个名为 Query.cs 的新类。
我们的查询类将包含一些方法,这些方法会返回一个 IQueryable Result 对象。我们将使用此查询类作为端点,从 API 获取返回的信息。
public class Query
{
// Will return all of our todo list items
// We are injecting the context of our dbConext to access the db
public IQueryable<ItemData> GetItem([Service] ApiDbContext context)
{
return context.Items;
}
}
现在我们需要在 ConfigureServices 方法中更新启动类,以使用 GraphQL,并为 GraphQL 创建入口点,同时提供模式构造。
services.AddGraphQLServer()
.AddQueryType<Query>();
现在我们需要更新我们的端点。
app.UseEndpoints(endpoints =>
{
endpoints.MapGraphQL();
});
app.UseGraphQLVoyager(new VoyagerOptions()
{
GraphQLEndPoint = "/graphql"
}, "/graphql-voyager");
让我们构建并运行我们的应用程序。
dotnet build
dotnet run
// http://localhost:5000/graphql
当我们访问http://localhost:5000/graphql时,可以看到我们正在使用由我们安装的 HotChocolate NuGet 包提供的用户界面。如果我们点击模式按钮(书本图标),可以看到我们添加的主查询,该查询基于我们在查询类中添加的查询,并被命名为 items。
让我们用 DBeaver 打开数据库,手动添加一些信息,然后返回到 URL http://localhost:5000/graphql。现在让我们测试一下应用程序。
query{
items
{
id
title
}
}
现在让我们更新查询并引入别名,这意味着我们希望在同一个查询中执行不同的命令,类似于下面这样。
query {
a:items{
id
title
}
b:items{
id
title
}
c:items{
id
title
}
}
在这个请求中,我们只收到 1 个请求的响应,而其他请求都出现错误,这是为什么呢?
主要原因是我们的应用程序数据库上下文不支持并行工作,这意味着当 GraphQL 尝试同时执行命令时会失败,因为数据库上下文只能单线程工作。
要解决这个问题,我们需要使用 .Net 5 中引入的一项新功能 PooledDbContextFactory,我们可以利用它来解决这个错误。
我们首先需要修改的是 ConfigureServices 方法中的启动类。
services.AddPooledDbContextFactory<ApiDbContext>(options =>
options.UseSqlite(
Configuration.GetConnectionString("DefaultConnection")
));
AddPooledDbContextFactory 本质上是创建 ApiDbContext 的实例并将其放入池中,每当需要数据库上下文时,我们可以从池中取出一个实例,并在使用完毕后将其返回。
下一步我们需要更新 Query.cs 文件。
// So basically this attribute is pulling a db context from a pool
// using the db context
// returning the db context to the pool
[UseDbContext(typeof(ApiDbContext))]
public IQueryable<ItemData> GetItems([ScopedService] ApiDbContext context)
{
return context.Items;
}
现在让我们再次运行应用程序并再次运行并行查询。
dotnet run
现在我们可以看到一切运行正常,接下来我们将尝试从列表中提取信息,所以我们将获取父列表以及属于该列表的所有项。
让我们在查询类中添加一个新查询。
[UseDbContext(typeof(ApiDbContext))]
public IQueryable<ItemList> GetLists([ScopedService] ApiDbContext context)
{
return context.Lists;
}
现在让我们尝试查询数据,看看能得到什么结果。为此,我们在 Insomnia 中创建了一个新的请求。
query{
lists
{
name
itemDatas {
id
title
}
}
}
我们可以看到,返回的数据并不完整。
那么我们该如何解决这个问题呢?我们需要在查询中启用投影,以便获取对象的子项。让我们将 Query.cs 类更新如下。
[UseDbContext(typeof(ApiDbContext))]
[UseProjection]
public IQueryable<ItemList> GetLists([ScopedService] ApiDbContext context)
{
return context.Lists;
}
接下来,我们需要在 ConfigureServices 方法中更新启动类。
// This will be the entry point and will provide us with a schema
// construction
services.AddGraphQLServer()
.AddQueryType<Query>()
.AddProjections();
文档
现在让我们来编写 API 文档。为此,我们需要更新模型类。首先,让我们更新 ItemData 模型。
[GraphQLDescription("Used to define todo item for a specific list")]
public class ItemData
{
public int Id { get; set; }
public string Title { get; set; }
public string Description { get; set; }
[GraphQLDescription("If the user has completed this item")]
public bool Done { get; set; }
[GraphQLDescription("The list which this item belongs to")]
public int ListId { get; set; }
public virtual ItemList ItemList { get; set; }
}
现在让我们更新物品列表
[GraphQLDescription("Used to group the do list item into groups")]
public class ItemList
{
public ItemList()
{
ItemDatas = new HashSet<ItemData>();
}
public int Id { get; set; }
public string Name { get; set; }
public virtual ICollection<ItemData> ItemDatas { get; set; }
}
现在,我们可以在哪里查看这些文档呢?由于我们已经添加了 GraphQL Voyager NuGet 包,并在启动类中进行了配置,因此我们需要访问以下 URL:http://localhost:5000/graphql-voyager
我们可以看到 API 的图形化表示,还可以看到我们添加的文档。
现在我们需要将模型和文档区分开来,为了实现这一点,我们将使用类型。
在 GraphQL 文件夹内,我们需要创建名为 Items 和 Lists 的新文件夹。创建好这些文件夹后,让我们在 Lists 文件夹中创建第一个类型文件 ListType.cs。
public class ListType : ObjectType<ItemList>
{
// since we are inheriting from objtype we need to override the functionality
protected override void Configure(IObjectTypeDescriptor<ItemList> descriptor)
{
descriptor.Description("Used to group the do list item into groups");
descriptor.Field(x => x.ItemDatas).Ignore();
descriptor.Field(x => x.ItemDatas)
.ResolveWith<Resolvers>(p => p.GetItems(default!, default!))
.UseDbContext<ApiDbContext>()
.Description("This is the list of to do item available for this list");
}
private class Resolvers
{
public IQueryable<ItemData> GetItems(ItemList list, [ScopedService] ApiDbContext context)
{
return context.Items.Where(x => x.ListId == list.Id);
}
}
}
现在我们需要在 GraphQL ⇒ Items 文件夹中创建 ItemType。
// since we are inheriting from objtype we need to override the functionality
protected override void Configure(IObjectTypeDescriptor<ItemData> descriptor)
{
descriptor.Description("Used to define todo item for a specific list");
descriptor.Field(x => x.ItemList)
.ResolveWith<Resolvers>(p => p.GetList(default!, default!))
.UseDbContext<ApiDbContext>()
.Description("This is the list that the item belongs to");
}
private class Resolvers
{
public ItemList GetList(ItemData item, [ScopedService] ApiDbContext context)
{
return context.Lists.FirstOrDefault(x => x.Id == item.ListId);
}
}
添加类型之后,我们需要更新启动类以利用这些类型,因此在 Startup 类的 ConfigureServices 方法中,我们需要更新为以下内容。
services.AddGraphQLServer()
.AddQueryType<Query>()
.AddType<ItemType>()
.AddType<ListType>()
.AddProjections();
现在让我们构建并运行应用程序,并检查其架构。
dotnet build
dotnet run
现在我们需要添加筛选和排序功能,我们需要将 Query 类更新如下。
public class Query
{
// Will return all of our todo list items
// We are injecting the context of our dbConext to access the db
// this is called a resolver
// So basically this attribute is pulling a db context from a pool
// using the db context
// returning the db context to the pool
[UseDbContext(typeof(ApiDbContext))]
[UseProjection] //=> we have remove it since we have used explicit resolvers
[UseFiltering]
[UseSorting]
public IQueryable<ItemData> GetItems([ScopedService] ApiDbContext context)
{
return context.Items;
}
[UseDbContext(typeof(ApiDbContext))]
[UseProjection] //=> we have remove it since we have used explicit resolvers
[UseFiltering]
[UseSorting]
public IQueryable<ItemList> GetLists([ScopedService] ApiDbContext context)
{
return context.Lists;
}
}
然后我们需要更新启动类的 ConfigureServices 方法
services.AddGraphQLServer()
.AddQueryType<Query>()
.AddType<ListType>()
.AddType<ItemType>()
.AddProjections()
.AddSorting()
.AddFiltering();
现在让我们创建一个带有筛选功能的新查询,看看筛选功能是如何工作的。
query {
lists(where: {id: {eq: 1} })
{
id
name
itemDatas {
title
}
}
}
排序查询
query{
lists(order: {name: DESC})
{
id
name
}
}
现在我们要介绍的是变更,变更是指我们想要添加、编辑和删除数据。
为了实现变更操作,我们需要在 GraphQL 文件夹中添加一个新类,这个变更类将包含两个方法:一个用于添加列表,另一个用于添加列表项。
首先,我们需要添加输入和输出模型,因此在 GraphQL ⇒ List 中,我们添加两个文件:AddListInput 和 AddListPayload。
public record AddListPayload(ListType list);
public record AddListInput(string name);
现在我们需要在 GraphQL 文件夹中添加我们的 mutation 类,我们添加一个名为 Mutation 的新类。
// this attribute will help us utilise the multi threaded api db context
[UseDbContext(typeof(ApiDbContext))]
public async Task<AddListPayload> AddListAsync(AddListInput input, [ScopedService] ApiDbContext context)
{
var list = new ItemList
{
Name = input.name
};
context.Lists.Add(list);
await context.SaveChangesAsync();
return new AddListPayload(list);
}
现在我们需要更新我们的创业课程
services.AddGraphQLServer()
.AddQueryType<Query>()
.AddType<ListType>()
.AddType<ItemType>()
.AddMutationType<Mutation>()
.AddProjections()
.AddSorting()
.AddFiltering();
现在让我们来测试一下,我们在 Insomnia 中创建一个新请求,并使用 mutation 而不是 query。
mutation{
addList(input: {
name: "Food"
})
{
list
{
name
}
}
}
现在我们要添加第二个 mutation 来添加列表项,所以首先在 GraphQL 中添加模型 ⇒ Items 将添加 AddItemInput 和 AddItemPayload。
public record AddItemInput(string title, string description, bool done, int listId);
public record AddItemPayload(ItemData item);
现在我们需要更新突变类
[UseDbContext(typeof(ApiDbContext))]
public async Task<AddItemPayload> AddItemAsync(AddItemInput input, [ScopedService] ApiDbContext context)
{
var item = new ItemData
{
Description = input.description,
Done = input.done,
Title = input.title,
ListId = input.listId
};
context.Items.Add(item);
await context.SaveChangesAsync();
return new AddItemPayload(item);
}
现在让我们来测试一下。
mutation{
addItem(input: {
title: "Bring laptop",
description: "Bring the laptop with charger",
done: true,
listId: 1
})
{
item
{
id
title
}
}
}
现在我们要了解一下订阅情况。
订阅是一种实时事件通知,我们利用 WebSocket 来实现这一点。
在 GraphQL 文件夹内将创建一个名为 Subscription.cs 的新类。
[Subscribe]
[Topic]
public ItemList OnListAdded([EventMessage] ItemList list) => list;
我们需要更新启动类以利用 WebSocket,我们用以下代码更新 Configure 方法。
app.UseWebSockets();
我们需要在启动类中更新的第二部分是 ConfigureServices 方法。
services.AddGraphQLServer()
.AddQueryType<Query>()
.AddType<ListType>()
.AddType<ItemType>()
.AddMutationType<Mutation>()
.AddSubscriptionType<Subscription>()
.AddProjections()
.AddSorting()
.AddFiltering()
.AddInMemorySubscriptions();
现在我们需要更新我们的变更机制,以便在添加新的列表项时,能够及时向订阅发送更新通知。
// this attribute will help us utilise the multi threaded api db context
[UseDbContext(typeof(ApiDbContext))]
public async Task<AddListPayload> AddListAsync(
AddListInput input,
[ScopedService] ApiDbContext context,
[Service] ITopicEventSender eventSender,
CancellationToken cancellationToken)
{
var list = new ItemList
{
Name = input.name
};
context.Lists.Add(list);
await context.SaveChangesAsync(cancellationToken);
// we emit our subscription
await eventSender.SendAsync(nameof(Subscription.OnListAdded), list, cancellationToken);
return new AddListPayload(list);
}



