iT邦幫忙

2024 iThome 鐵人賽

DAY 26
0
Software Development

DDD? Clean Architecture? Microservices? 帶你用.NET實作打造一個現代化微服務!系列 第 26

Day 26 - 打造靈活高效的 GraphQL 服務:從設計到實作

  • 分享至 

  • xImage
  •  

前言

先前在 Day 07 - gRPC 與 GraphQL 有介紹過,GraphQL 是一種查詢語言,用於 API,允許客戶端根據需求請求特定的數據,從而提升效率和靈活性。

在 Clean Architecture 中,GraphQL 屬於 Presentation 層,負責處理客戶端請求並與 Application 層交互,避免直接依賴資料存取。

產生 GraphQL 專案

GraphQL 與 gRPC 類似,都是最外部的 Presentation Layer,所以我們先在 src 資料夾下執行下方的 Scripts 來產生專案:

## Account GraphQL Project
cd .\Account\
dotnet new web -n Account.GraphQL
cd .\Account.GraphQL\
dotnet add package HotChocolate.AspNetCore
dotnet add package HotChocolate.Data
dotnet add .\Account.GraphQL.csproj reference ..\Account.Application\Account.Application.csproj 
dotnet add .\Account.GraphQL.csproj reference ..\Account.Infrastructure\Account.Infrastructure.csproj 
cd ..\..\

## Todo GraphQL Project
cd .\Todo\
dotnet new web -n Todo.GraphQL
cd .\Todo.GraphQL\
dotnet add package HotChocolate.AspNetCore
dotnet add package HotChocolate.Data
dotnet add .\Todo.GraphQL.csproj reference ..\Todo.Application\Todo.Application.csproj 
dotnet add .\Todo.GraphQL.csproj reference ..\Todo.Infrastructure\Todo.Infrastructure.csproj 
cd ..\..\

## Add Projects to Solution
dotnet sln add (ls -r **/*.csproj)

在這裡,我們使用 HotChocolate.AspNetCore 的套件來搭建 GraphQL 伺服器。這個套件提供了簡單易用的 API,讓我們能夠輕鬆建立 GraphQL 端點並處理查詢和資料篩選與變異。

由於 GraphQL 勢必會存取資料庫,故也需要 Reference Infrastructure Layer,以便透過資料訪問邏輯來取得所需的資料。

接下來,我們將設定 GraphQL 的基本結構,加入查詢(Query)和數據模型(Data Model),並將它們與 Application Layer 的 IRepository 進行整合。這樣一來,客戶端就能夠透過 GraphQL 發送請求,以獲取所需的數據,並同時保持應用的靈活性和效率。

HotChocolate Setup

我們先把基本的功能設置完成。

1. Create Query.cs

在 GraphQL 中,Query 是起頭的類型,我們可以在其中設定許多 Get Methods,用以定義客戶端可以發送的查詢。這樣的設計使得客戶端能夠靈活地獲取所需資料,並在一個請求中獲得所需的多個資料集。

namespace Account.GraphQL;

public class Query {}

2. Start up Setup

修改 Program.cs,這裡因為要使用到 Repository 來 Access Database,所以要 Add Application Layer 和 Infrastructure Layer,以便在 GraphQL 中使用這些層級的功能。

using Account.Application;
using Account.GraphQL;
using Account.Infrastructure;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddAccountApplication().AddAccountInfrastructure();

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

var app = builder.Build();

app.MapGraphQL();

app.Run();

最基本的設置到這裡就結束了,接下來我們只需要讓 Query 內有方法可以透過 Repository 拿到我們想要的資料,並轉譯成我們要的 DTO 即可。

Repository 改造

下一步我們來讓 Repository 有回傳 Entity 的功能。首先是在 IUserRepository 加入 GetUsers 的方法,這裡回傳 IQueryable 的原因是因為它允許延遲查詢,這樣我們可以在查詢過程中動態添加過濾條件或排序,而不會立即執行查詢。這使得資料庫操作更加高效,並提供了更大的靈活性。

public interface IUserRepository : IRepository<User>
{
    User? GetUserByEmail(string email);
    void Add(User user);
    IQueryable<User> GetUsers();
}

接著在 UserRepository 實作 GetUsers,這裡回傳的 DbSet 也是 IQueryable 類型,可以保留 LINQ 查詢的能力,讓我們可以利用 EF Core 的功能進行更複雜的查詢操作。

    public IQueryable<Domain.Aggregates.User> GetUsers()
    {
        return _accountContext.Users;
    }

新增 GetUsersQuery

接下來,我們要在 Query 類中新增一個 GetUsers 方法,以便客戶端能夠通過 GraphQL 查詢使用者資料。我們將從 IUserRepository 獲取 IQueryable,然後將其轉換為 DTO,以滿足 GraphQL 的要求。以下是方法的實作範例:

using Account.Application;
using Account.Domain.ValueObjects;
using Account.GraphQL.Models;

namespace Account.GraphQL;

public class Query
{
    [UseFiltering]
    public IQueryable<UserDto> GetUsers([Service] IUserRepository repository)
        => repository.GetUsers()
            .Select(user => new UserDto
            {
                Id = user.Id.Value.ToString(),
                FirstName = user.FirstName,
                LastName = user.LastName,
                Email = user.Email,
                CreatedDateTime = user.CreatedDateTime,
                UpdatedDateTime = user.UpdatedDateTime
            });
}

在這個方法中,我們將每個 User 實體轉換為 UserDto,這是一個用於 GraphQL 的數據傳輸物件,僅包含必要的字段,從而確保客戶端獲取到的資料精簡且高效。這樣一來,我們就能利用 GraphQL 提供的強大查詢功能,實現靈活的數據訪問。此外,[UseFiltering] 這個 Attribute 是 HotChocolate.Data 套件提供的自動產生 Filter 功能,可以使我們在 Query 時輕鬆 Filter 出我們想要的結果。

然後再完成 DTO 的類別,先產生 Models 資料夾,在其中 Create UserDto.cs 如下:

namespace Account.GraphQL.Models;

public class UserDto
{
    public string? Id { get; set; }
    public string? FirstName { get; set; }
    public string? LastName { get; set; }
    public string? Email { get; set; }
    public DateTime? CreatedDateTime { get; set; }
    public DateTime? UpdatedDateTime { get; set; }
}

另外我們也需要客製化一個 By UserId 拿回實體資料。

    public UserDto? GetUserById([Service] IUserRepository repository, string userId)
        => repository.GetUsers()
            .Where(user => user.Id == UserId.Create(new Guid(userId)))
            .Select(user => new UserDto
            {
                Id = user.Id.Value.ToString(),
                FirstName = user.FirstName,
                LastName = user.LastName,
                Email = user.Email,
                CreatedDateTime = user.CreatedDateTime,
                UpdatedDateTime = user.UpdatedDateTime
            }).FirstOrDefault();

這樣就完成了,超級簡單。

實測

dotnet run

先執行專案後到 http://localhost:[port]/graphql/ 就可以訪問 HotChocolate 幫我們設計好的 GraphQL 工具。

https://ithelp.ithome.com.tw/upload/images/20241010/20168953nzbjFYkbQa.png

我們 Create Document 後到 Schema Definition Tab 內看到我們創建的 Query 和 UserDto,其他的 Input 是套用 [UseFiltering] 後自動產生的 Filter 物件。

https://ithelp.ithome.com.tw/upload/images/20241010/20168953Id7ALsHevt.png

我們做一下簡單的查詢。

https://ithelp.ithome.com.tw/upload/images/20241010/20168953RijKIRctVs.png

在查詢內加入 Filter 找到我們的目標資料,要注意因為我們特別處理 DbContext Configuration 的關係,這裡 ValueObject 無法被 Where 條件解析。

https://ithelp.ithome.com.tw/upload/images/20241010/20168953ltGB7TRl28.png

甚至可以在將條件參數化,並寫在 Variables 內。

https://ithelp.ithome.com.tw/upload/images/20241010/20168953ITmwgSflvy.png

另外測試一下 GetuserById,會另外做這一個功能是因為 UserId 這個 ValueObject 被我們在 DbContext Configuration 中特別處理了,上面 Where 的 Filter Input 會使 SQL 語法出錯。

https://ithelp.ithome.com.tw/upload/images/20241010/20168953LpX9h94P7d.png

大功告成!

結語

在這個教學系列中,一度想要把 GraphQL 的介紹拿掉,因為 GraphQL 的靈活與高彈性,很容易造成大量的 Queries 在同一秒內產生,理論上我拿先前做好的 Repository 來當作 GraphQL 的實作有點不明智,不只因為 Aggregate 的 ValueObject 會造成 Filtering 的困擾,還有濫用 GraphQL 會有高併發的情況出現,現有的作法沒辦法很好的應付這種情況。GraphQL 的水深遠沒有我所介紹的這麼淺顯,效能優化和合理的架構設計對 GraphQL 來說是至關重要的,這裡只介紹了基本的應用,帶領讀者入門,之後的修行還是得靠個人。

另外上面 Account GraphQL Service 做完了,下一步的 Todo GraphQL Service 就請各位讀者實作看看,我在下方直接貼結果。下一章節我們會把 GraphQL 納入 Gateway 中。

Todo GraphQL Service 程式碼

Query

using Todo.Application;
using Todo.Domain.ValueObjects;
using Todo.GraphQL.Models;

namespace Todo.GraphQL.Queries;

public class Query {}

[ExtendObjectType(typeof(Query))]
public class TodoListQuery
{
    [UseFiltering]
    public IQueryable<TodoListDto> GetTodoLists([Service] ITodoListRepository todoListRepository, [Service] ITodoItemRepository todoItemRepository)
        => todoListRepository.GetTodoLists()
            .Select(todoList => new TodoListDto
            {
                Id = todoList.Id.Value.ToString(),
                Name = todoList.Name,
                Description = todoList.Description,
                Status = todoList.Status,
                UserId = todoList.UserId.ToString(),
                CreatedDateTime = todoList.CreatedDateTime,
                UpdatedDateTime = todoList.UpdatedDateTime
            });

    public TodoListDto? GetTodoListById([Service] ITodoListRepository todoListRepository, string listId)
        => todoListRepository.GetTodoLists()
            .Where(list => list.Id == TodoListId.Create(new Guid(listId)))
            .Select(todoList => new TodoListDto
            {
                Id = todoList.Id.Value.ToString(),
                Name = todoList.Name,
                Description = todoList.Description,
                Status = todoList.Status,
                UserId = todoList.UserId.ToString(),
                CreatedDateTime = todoList.CreatedDateTime,
                UpdatedDateTime = todoList.UpdatedDateTime
            }).FirstOrDefault();

    public List<TodoListDto>? GetTodoListsByUserId([Service] ITodoListRepository todoListRepository, string userId)
        => todoListRepository.GetTodoLists()
            .Where(list => list.UserId == new Guid(userId))
            .Select(todoList => new TodoListDto
            {
                Id = todoList.Id.Value.ToString(),
                Name = todoList.Name,
                Description = todoList.Description,
                Status = todoList.Status,
                UserId = todoList.UserId.ToString(),
                CreatedDateTime = todoList.CreatedDateTime,
                UpdatedDateTime = todoList.UpdatedDateTime
            }).ToList();
}

[ExtendObjectType(typeof(Query))]
public class TodoItemQuery
{
    [UseFiltering]
    public IQueryable<TodoItemDto> GetTodoItems([Service] ITodoItemRepository todoItemRepository)
        => todoItemRepository.GetTodoItems()
            .Select(todoItem => new TodoItemDto
            {
                Id = todoItem.Id.Value.ToString(),
                Content = todoItem.Content,
                State = todoItem.Status.State,
                Color = todoItem.Status.Color,
                ListId = todoItem.ListId.ToString(),
                CreatedDateTime = todoItem.CreatedDateTime,
                UpdatedDateTime = todoItem.UpdatedDateTime
            });

    public TodoItemDto? GetTodoItemById([Service] ITodoItemRepository todoItemRepository, string itemId)
        => todoItemRepository.GetTodoItems()
            .Where(item => item.Id == TodoItemId.Create(new Guid(itemId)))
            .Select(todoItem => new TodoItemDto
            {
                Id = todoItem.Id.Value.ToString(),
                Content = todoItem.Content,
                State = todoItem.Status.State,
                Color = todoItem.Status.Color,
                ListId = todoItem.ListId.ToString(),
                CreatedDateTime = todoItem.CreatedDateTime,
                UpdatedDateTime = todoItem.UpdatedDateTime
            }).FirstOrDefault();

    public List<TodoItemDto>? GetTodoItemsByListId([Service] ITodoItemRepository todoItemRepository, string listId)
        => todoItemRepository.GetTodoItems()
            .Where(item => item.ListId == new Guid(listId))
            .Select(todoItem => new TodoItemDto
            {
                Id = todoItem.Id.Value.ToString(),
                Content = todoItem.Content,
                State = todoItem.Status.State,
                Color = todoItem.Status.Color,
                ListId = todoItem.ListId.ToString(),
                CreatedDateTime = todoItem.CreatedDateTime,
                UpdatedDateTime = todoItem.UpdatedDateTime
            }).ToList();
}

TodoListDto

using Todo.Domain.ValueObjects.Enums;

namespace Todo.GraphQL.Models;

public class TodoListDto
{
    public string? Id { get; set; }
    public string? Name { get; set; }
    public string? Description { get; set; }
    public TodoListStatus? Status { get; set; }
    public string? UserId { get; set; }
    public DateTime? CreatedDateTime { get; set; }
    public DateTime? UpdatedDateTime { get; set; }
}

TodoItemDto

using Todo.Domain.ValueObjects.Enums;

namespace Todo.GraphQL.Models;

public class TodoItemDto
{
    public string? Id { get; set; }
    public string? Content { get; set; }
    public TodoItemState? State { get; set; }
    public string? Color { get; set; }
    public string? ListId { get; set; }
    public DateTime? CreatedDateTime { get; set; }
    public DateTime? UpdatedDateTime { get; set; }
}

Program

using Todo.Application;
using Todo.Infrastructure;
using Todo.GraphQL.Queries;
using HotChocolate.Stitching;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddTodoApplication().AddTodoInfrastructure();

builder.Services
    .AddGraphQLServer()
    .AddQueryType<Query>()
    .AddTypeExtension<TodoListQuery>()
    .AddTypeExtension<TodoItemQuery>()
    .AddFiltering();

var app = builder.Build();

app.MapGraphQL();

app.Run();

TodoListRepository

    public IQueryable<TodoList> GetTodoLists()
    {
        return _todoContext.TodoLists;
    }

TodoItemRepository

    public IQueryable<TodoItem> GetTodoItems()
    {
        return _todoContext.TodoItems;
    }

上一篇
Day 25 - BFF Gateway 實作:JWT Bearer Authentication 與 Authorization
下一篇
Day 27 - Backend 的最後一哩路:Gateway 與 GraphQL Stitching
系列文
DDD? Clean Architecture? Microservices? 帶你用.NET實作打造一個現代化微服務!30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言