MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

C#中的GraphQL查询语言实践

2023-09-185.0k 阅读

GraphQL 基础概念

GraphQL 是一种用于 API 的查询语言,由 Facebook 开发并开源。与传统的 RESTful API 不同,GraphQL 允许客户端精确地指定它需要的数据,而不是服务器预先定义的数据结构。这使得数据获取更加高效,减少了不必要的数据传输。

GraphQL 核心特性

  1. 查询(Queries):客户端通过编写查询语句来获取所需的数据。例如,如果有一个博客 API,客户端可以查询特定文章的标题、作者和内容:
query {
  article(id: "123") {
    title
    author
    content
  }
}
  1. 变更(Mutations):用于修改服务器端的数据。比如创建一篇新文章:
mutation {
  createArticle(input: {
    title: "New Article",
    author: "John Doe",
    content: "This is a new article"
  }) {
    id
    title
  }
}
  1. 订阅(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。

  1. 定义类型
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 的文档生成中很有用。

  1. 定义查询类型
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 从数据存储中获取数据。

  1. 定义变更类型
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 的输入参数,并创建一篇新文章。

  1. 创建 Schema
public class AppSchema : Schema
{
    public AppSchema(IServiceProvider serviceProvider) : base(serviceProvider)
    {
        Query = serviceProvider.GetRequiredService<Query>();
        Mutation = serviceProvider.GetRequiredService<Mutation>();
    }
}

AppSchemaQueryMutation 组合成一个完整的 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 查询实践

简单查询

  1. 获取所有文章: 在 GraphQL Playground 中,我们可以发送以下查询:
query {
  articles {
    id
    title
    author
  }
}

这个查询会返回所有文章的 idtitleauthor 字段。服务器端的解析过程如下:

  • GraphQL 服务器接收到查询请求。
  • 它找到 Query 类型中的 articles 字段。
  • articles 字段的解析器调用 IArticleRepositoryGetAllArticles 方法获取所有文章数据。
  • 数据经过序列化后返回给客户端。
  1. 根据 ID 获取单个文章
query {
  article(id: "123") {
    title
    content
  }
}

这个查询根据 id123 获取文章的 titlecontent 字段。服务器端的处理流程类似,只是 article 字段的解析器根据传入的 id 参数调用 GetArticleById 方法。

复杂查询

  1. 嵌套查询:假设我们的 Article 类型关联了 Comment 类型,并且 Commentauthortext 字段。我们可以进行嵌套查询:
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 导航属性,数据仓库方法需要正确加载关联的评论数据。

  1. 条件查询:我们可以在查询中添加条件,例如获取特定作者的文章:
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 变更实践

创建数据

  1. 创建文章
mutation {
  createArticle(input: {
    title: "New Article",
    author: "Jane Smith",
    content: "This is the content of the new article"
  }) {
    id
    title
  }
}

在服务器端,Mutation 类型的 createArticle 字段解析器会根据传入的 input 参数创建一篇新文章,并返回新文章的 idtitle。数据仓库的 CreateArticle 方法会将新文章保存到数据库中。

更新数据

  1. 更新文章:假设我们有一个 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 方法,实现类中编写更新数据库记录的逻辑。

删除数据

  1. 删除文章
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 订阅实践

实现订阅功能

  1. 设置订阅类型:假设我们要实现当有新评论发布到文章时的订阅功能。首先定义 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);
            }
        );
    }
}
  1. 事件发布与订阅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 方法会将评论推送给所有订阅了该文章新评论的客户端。

  1. 客户端订阅:在客户端,例如使用 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);
    }
});

这样,当有新评论发布到 articleId123 的文章时,客户端会收到新评论的数据并打印到控制台。

性能优化与最佳实践

数据加载优化

  1. 批处理:在解析 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 方法获取所有作者信息,大大减少了数据库查询次数。

  1. 缓存:可以在 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 分钟。如果在有效期内再次查询所有文章,直接从缓存中获取数据,提高了查询性能。

错误处理

  1. 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
                }
            ]
        }
    ]
}
  1. 全局错误处理:在.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 解析器中未显式处理的异常,也会以统一的格式返回给客户端,避免暴露敏感的服务器端错误信息。

安全最佳实践

  1. 身份验证与授权:在 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);
            }
        );

        // 其他字段定义...
    }
}
  1. 防止恶意查询: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 在高负载情况下依然保持良好的性能和稳定性。