造輪子之整合GraphQL

2023-10-16 18:00:23

先簡單對比以下GraphQL和WebAPI:
GraphQL和Web API(如RESTful API)是用於構建和提供Web服務的不同技術。

  1. 資料獲取方式:
  • Web API:通常使用RESTful API,使用者端通過傳送HTTP請求(如GET、POST、PUT、DELETE)來獲取特定的資料。每個請求通常返回一個固定的資料結構,包含在響應的主體中。
  • GraphQL:使用者端可以使用GraphQL查詢語言來精確指定需要的資料。使用者端傳送一個GraphQL查詢請求,伺服器根據查詢的結構和欄位來返回相應的資料。
  1. 資料獲取效率:
  • Web API:每個請求返回的資料通常是預定義的,無論使用者端需要的資料量大小,伺服器都會返回相同的資料結構。這可能導致使用者端獲取到不必要的資料,或者需要發起多個請求來獲取所需資料。
  • GraphQL:使用者端可以精確指定需要的資料,避免了不必要的資料傳輸。使用者端可以在一個請求中獲取多個資源,並且可以根據需要進行欄位選擇、過濾、排序等操作,從而提高資料獲取效率。
  1. 版本管理:
  • Web API:通常使用URL版本控制或者自定義的HTTP頭來管理API的版本。當API發生變化時,可能需要建立新的URL或者HTTP頭來支援新的版本。
  • GraphQL:GraphQL中沒有顯式的版本控制機制,而是通過向現有的型別和欄位新增新的欄位來擴充套件現有的API。這樣可以避免建立多個不同版本的API。
  1. 使用者端開發體驗:
  • Web API:使用者端需要根據API的檔案來構造請求和解析響應。使用者端需要手動處理不同的API端點和資料結構。
  • GraphQL:使用者端可以使用GraphQL的強型別系統和自動生成的程式碼工具來進行開發。使用者端可以根據GraphQL的模式自動生成型別定義和查詢程式碼,提供了更好的開發體驗和型別安全性。

在前面我們基礎框架是基於WebAPI(REST FUL API)的模式去開發介面的,所有的響應資料都需要定義一個DTO結構,但是有些場景可能只需要某些欄位,而後端又懶得定義新資料介面對接,這就會導致使用者端獲取到不必要的資料。在這種情況下,使用GraphQL就可以有較好的體驗。

那麼,在我們現有寫好的Service中,如何快速整合GraphQL又無需複雜編碼工作呢。這就是我們接下來要實現的了。

HotChocolate.AspNetCore

HotChocolate.AspNetCore是.NET一個老牌的GraphQL實現庫,它可以讓我們很快速的實現一個GraphQL Server。
安裝HotChocolate.AspNetCore的nuget,在Program中新增程式碼

builder.Services.AddGraphQLServer()
    
app.MapGraphQL();

這樣就完成一個GraphQLServer的整合。
啟動程式,存取https://localhost:7080/graphql/ 可以看到整合的介面。可以使用這個介面操作測試我們的graphql查詢。

實現QueryType

接下來實現一個基礎的QueryType,用於擴充套件查詢。

using HotChocolate.Authorization;

namespace Wheel.Graphql
{
    [Authorize]
    public class Query : IQuery
    {
    }

    [InterfaceType]
    public interface IQuery
    {

    }
}

在AddGraphQLServer()後面新增程式碼

builder.Services.AddGraphQLServer()
            .AddQueryType<Query>()
    ;

使用ExtendObjectType擴充套件Query類,方便介面拆分。

public interface IQueryExtendObjectType
{

}

[ExtendObjectType(typeof(IQuery))]
public class SampleQuery : IQueryExtendObjectType
{
    public List<string> Sample()
    {
        return new List<string> { "sample1", "sample2" };
    }
}
[ExtendObjectType(typeof(IQuery))]
public class Sample2Query : IQueryExtendObjectType
{
    public string Sample2(string id)
    {
        return id;
    }
}

這裡建立一個IQueryExtendObjectType空介面,用於獲取所有需要擴充套件的QueryAPI
約定所有擴充套件的Query需要繼承IQueryExtendObjectType介面,並加上ExtendObjectType特性標籤。
封裝AddGraphQLServer方法:

using HotChocolate.Execution.Configuration;
using System.Reflection;

namespace Wheel.Graphql
{
    public static class GraphQLExtensions
    {
        public static IRequestExecutorBuilder AddWheelGraphQL(this IServiceCollection services)
        {
            var result = services.AddGraphQLServer()
            .AddQueryType<Query>()
            ;

            var abs = Directory.GetFiles(AppDomain.CurrentDomain.BaseDirectory, "*.dll")
                        .Where(x => !x.Contains("Microsoft.") && !x.Contains("System."))
                        .Select(x => Assembly.Load(AssemblyName.GetAssemblyName(x))).ToArray();
            var types = abs.SelectMany(ab => ab.GetTypes()
                .Where(t => typeof(IQueryExtendObjectType).IsAssignableFrom(t) && typeof(IQueryExtendObjectType) != t));
            if (types.Any())
            {
                result = result.AddTypes(types.ToArray());
            }
            return result;
        }
    }
}

遍歷所有IQueryExtendObjectType並加入GraphQLServer。
啟動專案存取https://localhost:7080/graphql/
可以看到SchemaDefinition自動生成了我們的兩個查詢。

新增授權

安裝HotChocolate.AspNetCore.Authorization的Nuget包。
在services.AddGraphQLServer()後面新增程式碼.AddAuthorization()

using HotChocolate.Execution.Configuration;
using System.Reflection;

namespace Wheel.Graphql
{
    public static class GraphQLExtensions
    {
        public static IRequestExecutorBuilder AddWheelGraphQL(this IServiceCollection services)
        {
            var result = services.AddGraphQLServer()
            .AddAuthorization()
            .AddQueryType<Query>()
            ;

            var abs = Directory.GetFiles(AppDomain.CurrentDomain.BaseDirectory, "*.dll")
                        .Where(x => !x.Contains("Microsoft.") && !x.Contains("System."))
                        .Select(x => Assembly.Load(AssemblyName.GetAssemblyName(x))).ToArray();
            var types = abs.SelectMany(ab => ab.GetTypes()
                .Where(t => typeof(IQueryExtendObjectType).IsAssignableFrom(t) && typeof(IQueryExtendObjectType) != t));
            if (types.Any())
            {
                result = result.AddTypes(types.ToArray());
            }
            return result;
        }
    }
}

未登入前執行查詢,可以看到響應Error。

獲取一個token之後設定一下:

再次請求,可以看到正常查詢。

整合現有Service

改造一下SampleQuery

[ExtendObjectType(typeof(IQuery))]
public class SampleQuery : IQueryExtendObjectType
{
    public async Task<List<GetAllPermissionDto>> Sample([Service] IPermissionManageAppService permissionManageAppService)
    {
        var result = await permissionManageAppService.GetPermission();
        return result.Data;
    }
}

開啟https://localhost:7080/graphql/ 執行查詢,可以看到正常返回。

當我們需要過濾不查詢某些欄位時,只需要修改Query查詢格式。

分頁查詢,新增一下User的分頁查詢程式碼。

public class SampleQuery : IQueryExtendObjectType
{
    public async Task<List<GetAllPermissionDto>> Sample([Service] IPermissionManageAppService permissionManageAppService)
    {
        var result = await permissionManageAppService.GetPermission();
        return result.Data;
    }
    public async Task<Page<UserDto>> SampleUser(UserPageRequest pageRequest, [Service] IUserManageAppService userManageAppService)
    {
        var result = await userManageAppService.GetUserPageList(pageRequest);
        return result;
    }
}

測試:

可以看到,很簡單就可以把現有的API轉換成GraphQL。只不過一些排序分頁邏輯我們沒有采用GraphQL的方式,而是使用我們自己的WebApi分頁查詢的模式。

輪子倉庫地址https://github.com/Wheel-Framework/Wheel
歡迎進群催更。