C#中的GraphQL查询语言实践
GraphQL 基础概念
GraphQL 是一种用于 API 的查询语言,由 Facebook 开发并开源。与传统的 RESTful API 不同,GraphQL 允许客户端精确地指定它需要的数据,而不是服务器预先定义的数据结构。这使得数据获取更加高效,减少了不必要的数据传输。
GraphQL 核心特性
- 查询(Queries):客户端通过编写查询语句来获取所需的数据。例如,如果有一个博客 API,客户端可以查询特定文章的标题、作者和内容:
query {
article(id: "123") {
title
author
content
}
}
- 变更(Mutations):用于修改服务器端的数据。比如创建一篇新文章:
mutation {
createArticle(input: {
title: "New Article",
author: "John Doe",
content: "This is a new article"
}) {
id
title
}
}
- 订阅(Subscriptions):允许客户端实时接收服务器端数据变化的通知。例如,当有新评论发布到文章时,客户端可以实时获取:
subscription {
newCommentOnArticle(articleId: "123") {
author
content
}
}
在 C# 项目中集成 GraphQL
准备工作
在 C# 项目中使用 GraphQL,我们需要借助一些库。首先,我们使用 Microsoft.Extensions.DependencyInjection
来管理依赖注入,GraphQL.Server.Transports.AspNetCore
来搭建 GraphQL 服务器,以及 GraphQL.NewtonsoftJson
来处理 JSON 序列化和反序列化。
假设我们有一个简单的.NET Core Web 应用程序,使用以下命令安装必要的 NuGet 包:
dotnet add package GraphQL.Server.Transports.AspNetCore
dotnet add package GraphQL.NewtonsoftJson
创建 GraphQL Schema
在 C# 中,我们通过定义类型和解析器来创建 GraphQL Schema。
- 定义类型:
public class ArticleType : ObjectGraphType<Article>
{
public ArticleType()
{
Field(x => x.Id).Description("The unique identifier of the article.");
Field(x => x.Title).Description("The title of the article.");
Field(x => x.Author).Description("The author of the article.");
Field(x => x.Content).Description("The content of the article.");
}
}
这里,我们定义了一个 ArticleType
,它映射到我们的 Article
实体类。每个字段都有一个描述,这在 GraphQL 的文档生成中很有用。
- 定义查询类型:
public class Query : ObjectGraphType
{
public Query(IArticleRepository articleRepository)
{
Field<ListGraphType<ArticleType>>(
"articles",
resolve: context => articleRepository.GetAllArticles()
);
Field<ArticleType>(
"article",
arguments: new QueryArguments(
new QueryArgument<IdGraphType> { Name = "id" }
),
resolve: context =>
{
var id = context.GetArgument<string>("id");
return articleRepository.GetArticleById(id);
}
);
}
}
在 Query
类型中,我们定义了两个字段:articles
获取所有文章,article
根据 id
获取单个文章。解析器通过 IArticleRepository
从数据存储中获取数据。
- 定义变更类型:
public class Mutation : ObjectGraphType
{
public Mutation(IArticleRepository articleRepository)
{
Field<ArticleType>(
"createArticle",
arguments: new QueryArguments(
new QueryArgument<NonNullGraphType<ArticleInputType>> { Name = "input" }
),
resolve: context =>
{
var input = context.GetArgument<ArticleInput>("input");
var article = new Article
{
Title = input.Title,
Author = input.Author,
Content = input.Content
};
return articleRepository.CreateArticle(article);
}
);
}
}
Mutation
类型定义了 createArticle
字段,它接收一个 ArticleInputType
的输入参数,并创建一篇新文章。
- 创建 Schema:
public class AppSchema : Schema
{
public AppSchema(IServiceProvider serviceProvider) : base(serviceProvider)
{
Query = serviceProvider.GetRequiredService<Query>();
Mutation = serviceProvider.GetRequiredService<Mutation>();
}
}
AppSchema
将 Query
和 Mutation
组合成一个完整的 GraphQL Schema。
配置 GraphQL 服务器
在 Startup.cs
中,我们配置 GraphQL 服务器:
public void ConfigureServices(IServiceCollection services)
{
services.AddGraphQL(o => { o.ExposeExceptions = true; })
.AddGraphTypes(ServiceLifetime.Scoped)
.AddDataLoader()
.AddNewtonsoftJson();
services.AddScoped<Query>();
services.AddScoped<Mutation>();
services.AddScoped<AppSchema>();
services.AddScoped<IArticleRepository, ArticleRepository>();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseGraphQL<AppSchema>();
app.UseGraphQLPlayground(new GraphQLPlaygroundOptions());
}
在 ConfigureServices
方法中,我们注册了 GraphQL 相关的服务,包括 Schema、查询和变更类型,以及数据仓库。在 Configure
方法中,我们启用了 GraphQL 中间件,并提供了 GraphQL Playground,这是一个用于测试 GraphQL 查询的交互式界面。
GraphQL 查询实践
简单查询
- 获取所有文章: 在 GraphQL Playground 中,我们可以发送以下查询:
query {
articles {
id
title
author
}
}
这个查询会返回所有文章的 id
、title
和 author
字段。服务器端的解析过程如下:
- GraphQL 服务器接收到查询请求。
- 它找到
Query
类型中的articles
字段。 articles
字段的解析器调用IArticleRepository
的GetAllArticles
方法获取所有文章数据。- 数据经过序列化后返回给客户端。
- 根据 ID 获取单个文章:
query {
article(id: "123") {
title
content
}
}
这个查询根据 id
为 123
获取文章的 title
和 content
字段。服务器端的处理流程类似,只是 article
字段的解析器根据传入的 id
参数调用 GetArticleById
方法。
复杂查询
- 嵌套查询:假设我们的
Article
类型关联了Comment
类型,并且Comment
有author
和text
字段。我们可以进行嵌套查询:
query {
article(id: "123") {
title
comments {
author
text
}
}
}
要实现这个功能,我们需要在 ArticleType
中添加 comments
字段:
public class ArticleType : ObjectGraphType<Article>
{
public ArticleType()
{
// 其他字段定义...
Field<ListGraphType<CommentType>>(
"comments",
resolve: context => context.Source.Comments
);
}
}
在数据库层面,Article
实体类需要有一个 Comments
导航属性,数据仓库方法需要正确加载关联的评论数据。
- 条件查询:我们可以在查询中添加条件,例如获取特定作者的文章:
query {
articles(author: "John Doe") {
title
}
}
在 Query
类型中,我们修改 articles
字段的定义:
public class Query : ObjectGraphType
{
public Query(IArticleRepository articleRepository)
{
Field<ListGraphType<ArticleType>>(
"articles",
arguments: new QueryArguments(
new QueryArgument<StringGraphType> { Name = "author" }
),
resolve: context =>
{
var author = context.GetArgument<string>("author");
return articleRepository.GetArticlesByAuthor(author);
}
);
// 其他字段定义...
}
}
IArticleRepository
接口需要添加 GetArticlesByAuthor
方法,并在实现类中编写数据库查询逻辑。
GraphQL 变更实践
创建数据
- 创建文章:
mutation {
createArticle(input: {
title: "New Article",
author: "Jane Smith",
content: "This is the content of the new article"
}) {
id
title
}
}
在服务器端,Mutation
类型的 createArticle
字段解析器会根据传入的 input
参数创建一篇新文章,并返回新文章的 id
和 title
。数据仓库的 CreateArticle
方法会将新文章保存到数据库中。
更新数据
- 更新文章:假设我们有一个
updateArticle
变更:
mutation {
updateArticle(input: {
id: "123",
title: "Updated Article",
content: "This is the updated content"
}) {
id
title
}
}
在 Mutation
类型中定义 updateArticle
字段:
public class Mutation : ObjectGraphType
{
public Mutation(IArticleRepository articleRepository)
{
// 创建文章字段定义...
Field<ArticleType>(
"updateArticle",
arguments: new QueryArguments(
new QueryArgument<NonNullGraphType<ArticleUpdateInputType>> { Name = "input" }
),
resolve: context =>
{
var input = context.GetArgument<ArticleUpdateInput>("input");
var article = articleRepository.GetArticleById(input.Id);
if (article != null)
{
article.Title = input.Title;
article.Content = input.Content;
articleRepository.UpdateArticle(article);
}
return article;
}
);
}
}
ArticleUpdateInputType
定义了更新文章所需的输入参数。IArticleRepository
接口需要添加 UpdateArticle
方法,实现类中编写更新数据库记录的逻辑。
删除数据
- 删除文章:
mutation {
deleteArticle(id: "123") {
success
}
}
在 Mutation
类型中定义 deleteArticle
字段:
public class Mutation : ObjectGraphType
{
public Mutation(IArticleRepository articleRepository)
{
// 其他字段定义...
Field<BooleanGraphType>(
"deleteArticle",
arguments: new QueryArguments(
new QueryArgument<NonNullGraphType<IdGraphType>> { Name = "id" }
),
resolve: context =>
{
var id = context.GetArgument<string>("id");
var success = articleRepository.DeleteArticle(id);
return success;
}
);
}
}
IArticleRepository
接口需要添加 DeleteArticle
方法,实现类中编写从数据库删除文章记录的逻辑,并返回删除操作是否成功。
GraphQL 订阅实践
实现订阅功能
- 设置订阅类型:假设我们要实现当有新评论发布到文章时的订阅功能。首先定义
CommentType
:
public class CommentType : ObjectGraphType<Comment>
{
public CommentType()
{
Field(x => x.Id).Description("The unique identifier of the comment.");
Field(x => x.Author).Description("The author of the comment.");
Field(x => x.Text).Description("The text of the comment.");
}
}
然后在 Subscription
类型中定义订阅字段:
public class Subscription : ObjectGraphType
{
public Subscription(IObservableEventService eventService)
{
Field<CommentType>(
"newCommentOnArticle",
arguments: new QueryArguments(
new QueryArgument<NonNullGraphType<IdGraphType>> { Name = "articleId" }
),
resolve: context =>
{
var articleId = context.GetArgument<string>("articleId");
return eventService.GetNewCommentsOnArticle(articleId);
}
);
}
}
- 事件发布与订阅:
IObservableEventService
接口和实现类负责发布新评论事件。例如:
public interface IObservableEventService
{
IObservable<Comment> GetNewCommentsOnArticle(string articleId);
void PublishNewComment(Comment comment);
}
public class ObservableEventService : IObservableEventService
{
private readonly Dictionary<string, List<IObserver<Comment>>> _observers = new Dictionary<string, List<IObserver<Comment>>>();
public IObservable<Comment> GetNewCommentsOnArticle(string articleId)
{
return new Observable<Comment>(observer =>
{
if (!_observers.ContainsKey(articleId))
{
_observers[articleId] = new List<IObserver<Comment>>();
}
_observers[articleId].Add(observer);
return () => _observers[articleId].Remove(observer);
});
}
public void PublishNewComment(Comment comment)
{
if (_observers.ContainsKey(comment.ArticleId))
{
foreach (var observer in _observers[comment.ArticleId])
{
observer.OnNext(comment);
}
}
}
}
当有新评论创建时,PublishNewComment
方法会将评论推送给所有订阅了该文章新评论的客户端。
- 客户端订阅:在客户端,例如使用 Apollo Client 进行订阅:
import { ApolloClient, InMemoryCache, gql, subscribe } from '@apollo/client';
const client = new ApolloClient({
uri: 'http://localhost:5000/graphql',
cache: new InMemoryCache()
});
const subscription = gql`
subscription {
newCommentOnArticle(articleId: "123") {
author
text
}
}
`;
const unsubscribe = subscribe({ client, query: subscription }).subscribe({
next(data) {
console.log('New comment:', data.newCommentOnArticle);
},
error(err) {
console.error('Subscription error:', err);
}
});
这样,当有新评论发布到 articleId
为 123
的文章时,客户端会收到新评论的数据并打印到控制台。
性能优化与最佳实践
数据加载优化
- 批处理:在解析 GraphQL 查询时,如果有多个字段需要从数据库获取相关数据,我们可以使用批处理技术。例如,
Article
类型中有author
字段,而Author
信息存储在另一个表中。如果有多个文章查询,我们可以一次性获取所有文章的作者信息,而不是为每个文章单独查询作者。 在 C# 中,我们可以使用DataLoader
来实现批处理。首先安装GraphQL.DataLoader
NuGet 包。
dotnet add package GraphQL.DataLoader
然后在 Query
类型中使用 DataLoader
:
public class Query : ObjectGraphType
{
public Query(IArticleRepository articleRepository, IDataLoaderContextAccessor accessor)
{
Field<ListGraphType<ArticleType>>(
"articles",
resolve: context =>
{
var dataLoader = accessor.Context.GetOrAddCollectionBatchLoader<string, Author>(
"GetAuthorsByIds",
articleRepository.GetAuthorsByIds
);
var articles = articleRepository.GetAllArticles();
foreach (var article in articles)
{
article.AuthorLoader = dataLoader.LoadAsync(article.AuthorId);
}
return articles;
}
);
// 其他字段定义...
}
}
在 ArticleType
中,我们修改 author
字段的解析逻辑:
public class ArticleType : ObjectGraphType<Article>
{
public ArticleType()
{
// 其他字段定义...
Field<AuthorType>(
"author",
resolve: async context =>
{
var authorLoader = context.Source.AuthorLoader;
return await authorLoader;
}
);
}
}
这样,当查询多个文章及其作者时,DataLoader
会将所有文章的 AuthorId
收集起来,一次性调用 GetAuthorsByIds
方法获取所有作者信息,大大减少了数据库查询次数。
- 缓存:可以在 GraphQL 服务器端实现缓存机制。对于一些不经常变化的数据查询,例如网站的配置信息,我们可以将查询结果缓存起来。在 C# 中,可以使用
MemoryCache
或分布式缓存如Redis
。
public class Query : ObjectGraphType
{
private readonly IMemoryCache _memoryCache;
private readonly IArticleRepository _articleRepository;
public Query(IMemoryCache memoryCache, IArticleRepository articleRepository)
{
_memoryCache = memoryCache;
_articleRepository = articleRepository;
Field<ListGraphType<ArticleType>>(
"articles",
resolve: context =>
{
return _memoryCache.GetOrCreate("allArticles", entry =>
{
entry.SlidingExpiration = TimeSpan.FromMinutes(5);
return _articleRepository.GetAllArticles();
});
}
);
// 其他字段定义...
}
}
这里使用 MemoryCache
缓存了所有文章的查询结果,缓存有效期为 5 分钟。如果在有效期内再次查询所有文章,直接从缓存中获取数据,提高了查询性能。
错误处理
- GraphQL 错误处理:GraphQL 有自己的错误处理机制。当查询或变更执行过程中发生错误时,服务器会返回包含错误信息的响应。例如,当根据
id
查询文章时,如果文章不存在,我们可以返回错误:
public class Query : ObjectGraphType
{
public Query(IArticleRepository articleRepository)
{
Field<ArticleType>(
"article",
arguments: new QueryArguments(
new QueryArgument<IdGraphType> { Name = "id" }
),
resolve: context =>
{
var id = context.GetArgument<string>("id");
var article = articleRepository.GetArticleById(id);
if (article == null)
{
context.Errors.Add(new ExecutionError($"Article with id {id} not found."));
return null;
}
return article;
}
);
// 其他字段定义...
}
}
客户端接收到的响应会包含错误信息:
{
"data": {
"article": null
},
"errors": [
{
"message": "Article with id 123 not found.",
"locations": [
{
"line": 2,
"column": 3
}
]
}
]
}
- 全局错误处理:在.NET Core 应用程序中,我们可以设置全局错误处理中间件来捕获未处理的异常,并将其转换为 GraphQL 友好的错误响应。在
Startup.cs
中:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseExceptionHandler(errorApp =>
{
errorApp.Run(async context =>
{
var exception = context.Features.Get<IExceptionHandlerFeature>()?.Error;
var graphQLError = new ExecutionError("Internal server error", exception);
var response = new ExecutionResult { Errors = new[] { graphQLError } };
await context.Response.WriteAsJsonAsync(response);
});
});
// 其他配置...
}
这样,即使在 GraphQL 解析器中未显式处理的异常,也会以统一的格式返回给客户端,避免暴露敏感的服务器端错误信息。
安全最佳实践
- 身份验证与授权:在 GraphQL API 中,我们需要确保只有授权的用户才能执行某些查询或变更。可以在 GraphQL 中间件之前添加身份验证和授权中间件。例如,使用 JWT 进行身份验证:
在
Startup.cs
中:
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = Configuration["Jwt:Issuer"],
ValidAudience = Configuration["Jwt:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["Jwt:Key"]))
};
});
services.AddAuthorization(options =>
{
options.AddPolicy("AdminOnly", policy => policy.RequireRole("Admin"));
});
// GraphQL 相关服务配置...
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseAuthentication();
app.UseAuthorization();
// GraphQL 相关配置...
}
然后在 Mutation
类型中,对某些变更进行授权检查:
public class Mutation : ObjectGraphType
{
public Mutation(IArticleRepository articleRepository)
{
Field<ArticleType>(
"createArticle",
arguments: new QueryArguments(
new QueryArgument<NonNullGraphType<ArticleInputType>> { Name = "input" }
),
resolve: context =>
{
var user = context.User;
if (!user.IsInRole("Admin"))
{
context.Errors.Add(new ExecutionError("Only admins can create articles."));
return null;
}
var input = context.GetArgument<ArticleInput>("input");
var article = new Article
{
Title = input.Title,
Author = input.Author,
Content = input.Content
};
return articleRepository.CreateArticle(article);
}
);
// 其他字段定义...
}
}
- 防止恶意查询:GraphQL 查询可以非常灵活,但也可能被恶意利用来发起复杂的查询,导致性能问题。我们可以设置查询复杂度限制。在
Startup.cs
中:
public void ConfigureServices(IServiceCollection services)
{
services.AddGraphQL(o =>
{
o.ExposeExceptions = true;
o.ComplexityConfiguration = new ComplexityConfiguration
{
MaxDepth = 5,
FieldComplexity = new Dictionary<string, Func<FieldComplexityContext, int>>
{
{ "articles", context => context.Field.Arguments.Count * 2 }
}
};
})
.AddGraphTypes(ServiceLifetime.Scoped)
.AddDataLoader()
.AddNewtonsoftJson();
// 其他服务配置...
}
这里设置了最大查询深度为 5,并且对 articles
字段根据其参数数量计算复杂度。如果查询复杂度超过限制,GraphQL 服务器会返回错误,防止恶意查询耗尽服务器资源。
通过以上在 C# 中对 GraphQL 的实践,我们可以构建高效、灵活且安全的 API,满足现代应用程序复杂的数据获取和操作需求。无论是简单的查询,还是复杂的变更和实时订阅,GraphQL 与 C# 的结合都提供了强大的解决方案。同时,通过性能优化和遵循最佳实践,可以确保 API 在高负载情况下依然保持良好的性能和稳定性。