Xây dựng ứng dụng GraphQL với ASP.Net Core và TypeScript

GraphQL (QL: Query Language) là một ngôn ngữ truy vấn cho các API của bạn. Trong một thời gian dài, các ứng dụng client đã được chỉ định định dạng dữ liệu mà họ có thể nhận được từ API backend.

Ví dụ: nếu client gửi yêu cầu GET tới điểm cuối này: https://api.twitter.com/1.1/statuses/home_timeline.json (xem hướng dẫn dành cho nhà phát triển), nó sẽ lấy tất cả dữ liệu ở định dạng được quy định bởi API mà sau đó client sẽ phải lọc để chỉ lấy các trường mong muốn, ví dụ: Tweet text.

Cách tiếp cận REST API có nhiều nhược điểm mà chúng ta sẽ thảo luận ngay sau đây. Tuy nhiên, bây giờ chúng ta hãy xem nhanh một truy vấn GraphQL điển hình.

Vì GraphQL là một ngôn ngữ truy vấn, hãy so sánh một truy vấn GraphQL với một truy vấn bằng ngôn ngữ truy vấn rất nổi tiếng - SQL. Với GraphQL, bạn có thể viết các truy vấn bằng cách sử dụng cấu trúc đối tượng thay vì biểu thức chuỗi. Vì vậy, câu lệnh sau trong SQL:

SELECT name, id FROM employees WHERE id = 1

Có thể được dịch sang trong GraphQL như sau:

{
  employees(id: 1) {
    id
    name
  }
}

Trong các ứng dụng tiêu chuẩn, client thực hiện một số yêu cầu đối với một API để lấy dữ liệu có liên quan hoặc dựa vào ứng dụng BFF (Backend For Frontend) cho cùng một ứng dụng. Ngay cả với BFF, client cần biết các điểm cuối khác nhau của BFF mà nó có thể sử dụng để lấy dữ liệu liên quan.

Với GraphQL, một client chỉ cần gửi yêu cầu đến một điểm cuối duy nhất /graphql với một query (hoặc hoạt động thao tác dữ liệu được gọi là mutation) làm đầu vào. Máy chủ trả lời query theo định dạng do client yêu cầu.

Sơ đồ sau minh họa kiến ​​trúc của một hệ thống dựa trên GraphQL điển hình. Máy khách chỉ cần gửi yêu cầu đến một điểm cuối duy nhất và các trình xử lý hoặc trình phân giải khác nhau trên máy chủ sẽ xử lý yêu cầu.

Kiến trúc GraphQL
Kiến trúc GraphQL

Ưu điểm của GraphQL

Sử dụng GraphQL, độ phức tạp của client giảm đáng kể vì nó không còn cần phải hiểu các HTTP Verb, điểm cuối và đường dẫn yêu cầu khác nhau. Vì máy chủ chỉ có thể trả lời các query của client ở một định dạng cụ thể, nên client không còn cần phải hiểu các mã phản hồi khác nhau của API nữa.

Một vấn đề khác mà GraphQL giải quyết là tìm nạp quá nhiều dữ liệu. Sử dụng GraphQL, chỉ dữ liệu mà client quan tâm mới được trả về dưới dạng phản hồi cho một request.

Mặt khác, GraphQL cũng giải quyết vấn đề tìm nạp dữ liệu quá mức còn được gọi là vấn đề N + 1. Ví dụ: với các dịch vụ REST API, client có thể cần thực hiện nhiều request để có được dữ liệu liên quan, chẳng hạn như dữ liệu cho một sản phẩm và sau đó là thông tin chi tiết về sản phẩm.

Vì client và máy chủ rất đơn giản, các ứng dụng có thể được phát triển lặp đi lặp lại và các thay đổi trên client không yêu cầu thay đổi đối với máy chủ trong hầu hết các trường hợp. Mã phía máy chủ có thể được giám sát độc lập và các đơn vị dữ liệu không bao giờ được client sử dụng có thể không được dùng nữa mà không ảnh hưởng đến client.

GraphQL cũng hỗ trợ cập nhật dữ liệu thông qua các hoạt động được gọi mutation. Ở phần sau của bài viết này, chúng ta sẽ thấy cách chúng ta không phải viết bất kỳ mã nào cho các phiên bản query khác nhau và các phiên bản mutation khác nhau và toàn bộ quá trình này khá đơn giản.

Tình huống

Để chứng minh khả năng của GraphQL, chúng ta sẽ xây dựng một ứng dụng đơn giản liệt kê và thêm các trích dẫn từ các nhân vật nổi tiếng. Giao diện người dùng của ứng dụng được xây dựng bằng TypeScript sử dụng các query và mutation GraphQL để tương tác với API GraphQL backend được xây dựng bằng ASP.NET Core WebAPI.

Trong phần đầu tiên của loạt bài này, chúng ta sẽ tập trung vào việc xây dựng API GraphQL backend và trong phần tiếp theo, chúng ta sẽ làm việc với client dựa trên TypeScript.

Máy chủ GraphQL

Chúng ta sẽ sử dụng mẫu ASP.NET Core WebAPI để xây dựng API GraphQL của mình. Chúng ta sẽ làm theo các bước sau để tạo API của mình. Chúng ta sẽ trình bày chi tiết về các query và mutation sau khi chúng ta đã tạo xong API của mình.

  1. Cài đặt gói GraphQL trong ứng dụng của bạn.
  2. Tạo và thêm dữ liệu vào cơ sở dữ liệu ứng dụng.
  3. Tạo query, mutation và trình phân giải được API hỗ trợ.
  4. Cấu hình định tuyến cho GraphQL để client có thể truy cập tại điểm cuối /graphql.

Cài đặt gói GraphQL

Trong IDE của bạn, hãy tạo một dự án ASP.NET Core WebAPI trống. Trong dự án này, chúng ta sẽ thêm một middleware mới để hỗ trợ GraphiQL. GraphiQL (phát âm là “graphical”) thêm hỗ trợ IDE trong trình duyệt cho GraphQL. Hãy tưởng tượng GraphiQL là giao diện người dùng Swagger cho API của bạn.

Để thêm hỗ trợ cho GraphQL trong ứng dụng của bạn, hãy thực hiện lệnh sau để cài đặt gói GraphQL.

dotnet add package GraphQL

Thực thi lệnh sau để cài đặt gói graphiql trong ứng dụng của bạn.

dotnet add package graphiql

Thêm câu lệnh sau vào phương thức Configure trong lớp Startup. Điều này sẽ thêm middleware GraphiQL vào ứng dụng và làm cho giao diện người dùng GraphiQL khả dụng tại điểm cuối /graphql.

app.UseGraphiQl("/graphql");

Sau khi cấu hình các gói này, bây giờ chúng ta đã sẵn sàng chuẩn bị cơ sở dữ liệu mà client sẽ thao tác.

Chuẩn bị cơ sở dữ liệu

Sử dụng Entity Framework Core là cách đơn giản nhất để tạo các thực thể có thể truy vấn cho GraphQL. Hãy bắt đầu với việc xây dựng các mô hình cho ứng dụng của chúng ta. Tạo một thư mục có tên Models và thêm lớp có tên Author vào đó.

public class Author
{
  public int Id { get; set; }
  public string Name { get; set; }
  public List<Quote> Quotes { get; set; }
}

Bây giờ, thêm một lớp khác có tên Quotes vào thư mục.

public class Quote
{
  public string Id { get; set; }
  public string Text { get; set; }
  public string Category { get; set; }
  public int AuthorId { get; set; }
  public Author Author { get; set; }
}

Sau khi thêm các lớp mô hình, chúng ta sẽ tạo một lớp DbContext để cung cấp cầu nối giữa các thực thể của bạn và cơ sở dữ liệu. Thêm lớp có tên ApplicationDbContext vào project của bạn và thêm mã sau vào lớp đó.

public class ApplicationDbContext : DbContext
{
    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options) { }

    public DbSet<Quote> Quotes { get; set; }
    public DbSet<Author> Authors { get; set; }
}

Bây giờ chúng ta hãy kết nối DbContext với ứng dụng. Đối với ví dụ này, tôi sẽ sử dụng cơ sở dữ liệu trong bộ nhớ (InMemory). Tuy nhiên, bạn có thể cấu hình bất kỳ cơ sở dữ liệu nào được EF Core hỗ trợ. Thêm mã sau vào phương thức ConfigureServices của lớp Startup.

services.AddDbContext<ApplicationDbContext>(context =>
{
    context.UseInMemoryDatabase("QuoTSDb");
});

Hãy thêm một số dữ liệu vào cơ sở dữ liệu để cho client làm việc khi ứng dụng khởi động. Thêm mã sau vào phương thức Main của lớp Program để thêm một số bản ghi vào cơ sở dữ liệu.

public static void Main(string[] args)
{
    IWebHost host = CreateWebHostBuilder(args).Build();
    using(IServiceScope scope = host.Services.CreateScope())
    {
        ApplicationDbContext context = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();

        var ablDbEntry = context.Authors.Add(new Author { Name = "Abraham Lincoln" });
        var aristotleDbEntry = context.Authors.Add(new Author { Name = "Aristotle" });

        context.Quotes.AddRange(
            new Quote
            {
                AuthorId = ablDbEntry.Entity.Id,
                    Category = "inspiration",
                    Text = "Whatever you are, be a good one."
            },
            new Quote
            {
                AuthorId = ablDbEntry.Entity.Id,
                    Category = "books",
                    Text = "My Best Friend is a person who will give me a book I have not read."
            },
            new Quote
            {
                AuthorId = aristotleDbEntry.Entity.Id,
                    Category = "inspiration",
                    Text = "You will never do anything in this world without courage. It is the greatest quality of the mind next to honor."
            }
        );

        context.SaveChanges();
    }
    host.Run();
}

Tại thời điểm này, chúng ta đã sẵn sàng với hai thực thể có thể truy vấn và một cơ sở dữ liệu đầy đủ chức năng. Tuy nhiên, chúng ta chưa thể sử dụng lớp AuthorQuote trong các truy vấn GraphQL của mình vì chúng cần được chỉ định ở định dạng mà GraphQL hiểu được. Do đó, chúng ta sẽ thực hiện chuyển đổi các thực thể trong bước tiếp theo.

Tạo các kiểu Query GraphQL và Query

Dịch vụ GraphQL yêu cầu xác định các kiểu và trường trên các kiểu đó. Hơn nữa, bạn cung cấp các chức năng cho từng trường trên từng kiểu. Trong GraphQL, lược đồ được đặt trên máy chủ và client sử dụng lược đồ để truy vấn hoặc thay đổi (tạo, cập nhật và xóa) dữ liệu qua một điểm cuối duy nhất.

Lược đồ dữ liệu được xác định theo một định dạng cụ thể bằng cách sử dụng một đặc tả được gọi là Schema Definition Language. Sau đây là một ví dụ về cách lược đồ Author trong mẫu của chúng ta sẽ được xác định.

{
  author: Author
  authors: GraphQL List (Author)
}

Trong lược đồ được ở trên, Author chính nó là một Kiểu với ba trường là: Id, NameQuotes. Nó sẽ được biểu diễn như sau.

{
  Author = {
    id: GraphQL Int
    name: GraphQL String
    quotes: GraphQL List (Quote)
  }
}

Ngoài ra, Quote bản thân nó là một kiểu khác. Client GraphQL chỉ hiểu lược đồ và các kiểu và do đó, chúng ta cần chuyển đổi các các lớp AuthorQuote thành các kiểu.

Trong project, hãy tạo một thư mục có tên GraphQL và thêm một lớp có tên AuthorType vào đó. Tất cả các kiểu nên kế thừa từ lớp ObjectGraphType<T>. Thêm mã sau vào lớp để ánh xạ các thuộc tính của lớp Author thành các kiểu GraphQL như chuỗi, số nguyên, v.v.

public class AuthorType : ObjectGraphType<Author>
{
    public AuthorType()
    {
        Name = nameof(Author);

        Field(x => x.Id, type : typeof(IdGraphType)).Description("Author Id.");
        Field(x => x.Name).Description("The name of the author.");
        Field(x => x.Quotes, type : typeof(ListGraphType<QuoteType>)).Description("Author's quotes.");
    }
}

Tương tự, thêm một lớp khác có tên QuoteType vào thư mục và cập nhật mã của lớp này với mã trong danh sách sau.

public class QuoteType : ObjectGraphType<Quote>
{
    public QuoteType()
    {
        Name = nameof(Quote);
        Field(x => x.Id, type : typeof(IdGraphType)).Description("The Id of the quote.");
        Field(x => x.Text).Description("The quote.");
        Field(x => x.Category).Description("Quote category");
    }
}

Sau khi xác định lược đồ, bây giờ chúng ta có thể xác định các query mà client có thể thực hiện bằng cách sử dụng lược đồ. Máy chủ trình bày lược đồ các query và mutation cho client mà nó có thể gửi đến máy chủ.

Chúng ta sẽ xác định một query để lấy một đối tượng Author khi client gửi id của tác giả làm đối số của query. Trong GraphQL, query sẽ giống như sau. Lưu ý rằng chúng ta chỉ yêu cầu gửi các trích dẫn từ tác giả trong phản hồi.

{
  author(id: 1) {
    quotes {
      text
    }
  }
}

Phản hồi cho query này sẽ giống như sau.

{
  "data": {
    "author": {
      "quotes": [
        {
          "text": "Whatever you are, be a good one."
        },
        {
          "text": "My Best Friend is a person who will give me a book I have not read."
        }
      ]
    }
  }
}

Thêm một tệp lớp khác có tên AuthorQuery trong thư mục GraphQL. Chúng ta sẽ xác định hai truy vấn trong lớp này:

  1. Author: Tìm nạp đối tượng tác giả có id được truyền vào đối số của query.
  2. Authors: Tìm nạp tất cả các tác giả.

Trước khi chúng ta viết bất kỳ mã nào trong lớp này, tôi muốn nói về các khái niệm về trình phân giải (Resolver). Trách nhiệm của trình phân giải là truy xuất dữ liệu từ tài nguyên dữ liệu như API hoặc cơ sở dữ liệu và tạo đối tượng trả về dưới dạng phản hồi. Thêm mã sau vào lớp để cung cấp các truy vấn và trình phân giải của chúng.

public class AuthorQuery : ObjectGraphType
{
    public AuthorQuery(ApplicationDbContext db)
    {
        Field<AuthorType>(
            nameof(Author),
            arguments : new QueryArguments(new QueryArgument<IdGraphType> { Name = "id", Description = "The Id of the Author." }),
            resolve : context =>
            {
                var id = context.GetArgument<int>("id");
                var author = db
                    .Authors
                    .Include(a => a.Quotes)
                    .FirstOrDefault(i => i.Id == id);
                return author;
            });

        Field<ListGraphType<AuthorType>>(
            $"{nameof(Author)}s",
            resolve : context =>
            {
                var authors = db.Authors.Include(a => a.Quotes);
                return authors;
            });
    }
}

Tạo Mutation GraphQL

Trong khi chúng ta đang trong quá trình viết query, hãy thêm thao tác mutation để thêm trích dẫn vào hồ sơ tác giả. Mutation là các đối tượng GraphQL tương tự như query, nhưng bằng cách sử dụng mutation bạn có thể cập nhật, xóa và tạo bản ghi.

Sau đây là một ví dụ về thao tác mutation có tên createQuote chấp nhận các tham số để tạo trích dẫn mới. Thao tác mutation cũng cho phép bạn truy vấn đối tượng được cập nhật như một phần của thao tác.

Trong ví dụ sau, bạn có thể thấy rằng chúng ta có thể yêu cầu các trường khác nhau của đối tượng kết quả Author được trả về để phản hồi lại thao tác mutation.

mutation {
  createQuote(
    quote: {
      authorId: 2
      text: "Pleasure in the job puts perfection in the work."
      category: "job"
    }
  ) {
    name
    quotes {
      text
      category
    }
  }
}

Bây giờ chúng ta sẽ định nghĩa một lớp có tên QuoteInput đóng gói các tham số mà chúng ta cần để tạo một trích dẫn mới. Thêm một tệp lớp mới có tên QuoteInput trong thư mục GraphQL và thêm ba thuộc tính trong lớp như được hiển thị trong đoạn mã sau.

public class QuoteInput
{
    public int AuthorId { get; set; }
    public string Category { get; set; }
    public string Text { get; set; }
}

GraphQL không hiểu các lớp .Net nên cũng giống như trước, chúng ta sẽ chuyển đổi QuoteInput sang kiểu GraphQL. Một đầu vào nên mở rộng lớp InputObjectGraphType để trình bày kiểu ở định dạng mà GraphQL hiểu được. Thêm một lớp mới có tên QuoteInputType vào thư mục và dán mã sau vào tệp.

public class QuoteInputType : InputObjectGraphType<QuoteInput>
{
  public QuoteInputType()
  {
    Name = $"{nameof(QuoteInput)}";
    Field(x => x.AuthorId).Description("Author id.");
    Field(x => x.Text).Description("Quote text.");
    Field(x => x.Category).Description("Quote category.");
  }
}

Cuối cùng, chúng ta sẽ xác định một thao tác mutation để thêm một bản ghi vào cơ sở dữ liệu từ đầu vào mà nó nhận được từ tham số hàm mutation.

public class QuoteMutation : ObjectGraphType
{
    public QuoteMutation(ApplicationDbContext db)
    {
        Field<AuthorType>(
            $"create{nameof(Quote)}",
            arguments : new QueryArguments(new QueryArgument<NonNullGraphType<QuoteInputType>> { Name = "quote", Description = "Quote to add to author profile." }),
            resolve : context =>
            {
                var quote = context.GetArgument<QuoteInput>("quote");
                var author = db
                    .Authors
                    .Include(a => a.Quotes)
                    .FirstOrDefault(i => i.Id == quote.AuthorId);
                author.Quotes.Add(new Quote { Category = quote.Category, Text = quote.Text });
                db.SaveChanges();
                return author;
            });
    }
}

Trong lớp QuoteMutation, chúng ta đã xác định một thao tác mutation có tên createQuote chấp nhận một tham số có tên là quote kiểu QuoteInputType. Hàm phân giải trích xuất các thuộc tính từ tham số quote và sử dụng dữ liệu để thêm trích dẫn mới vào hồ sơ tác giả.

Hiển thị điểm cuối

Cho dù là query hay mutation, client sẽ luôn gửi một yêu cầu POST tới điểm cuối GraphQL (/graphql), nó sẽ chứa tên của query, tên của hoạt động và các tham số. Chúng ta sẽ tạo một lớp đóng vai trò như một mô hình cho tất cả các query và mutation. Tạo một lớp có tên GraphQLQuery và cập nhật mã của lớp như sau.

public class GraphQLQuery
{
    public string OperationName { get; set; }
    public string Query { get; set; }
    public JObject Variables { get; set; }
}

Cuối cùng, chúng ta sẽ tạo một điểm cuối POST mà client có thể gửi yêu cầu. Tạo controller mới có tên GraphQLController trong API của bạn và dán mã sau vào lớp.

[Route("graphql")]
[ApiController]
public class GraphQLController : ControllerBase
{
    private readonly ApplicationDbContext _db;

    public GraphQLController(ApplicationDbContext db) => _db = db;

    public async Task<IActionResult> Post([FromBody] GraphQLQuery query)
    {
        // Convert parameters to Dictionary<string,object>
        var inputs = query.Variables.ToInputs();

        // This is the schema for our GraphQL service. You can visualize it in the GraphiQL interface.
        var schema = new Schema
        {
            Query = new AuthorQuery(_db),
            Mutation = new QuoteMutation(_db)
        };

        // This function will either execute query or mutation based on request.
        var result = await new DocumentExecuter().ExecuteAsync(_ =>
        {
            _.Schema = schema;
            _.Query = query.Query;
            _.OperationName = query.OperationName;
            _.Inputs = inputs;
        });

        if (result.Errors?.Count > 0)
        {
            return BadRequest();
        }

        return Ok(result);
    }
}

Trong đoạn mã trên, chúng ta đã xác định một lược đồ sẽ được dịch sang định nghĩa lược đồ bằng cách sử dụng các tiêu chuẩn của Schema Definition Language. Lưu ý rằng các query và mutation có một gốc duy nhất. Điều này có nghĩa là tất cả các query sẽ được quản lý bởi lớp AuthorQuery và tất cả các mutationsẽ được quản lý bởi lớp QuoteMutation.

Kiểm tra ứng dụng

Khởi chạy ứng dụng ngay bây giờ và di chuyển đến URL: https://localhost:5001/graphql/. Giao diện GraphiQL được chia thành ba phần:

  • Phần ngoài cùng bên trái là cửa sổ soạn thảo thông minh hỗ trợ tính năng tự động điền và định dạng. Bạn có thể viết các query, mutation và biến được chúng sử dụng trong phần đầu tiên.
  • Phần giữa trình bày kết quả của thao tác.
  • Phần bên phải hiển thị tài liệu của API dựa trên Định nghĩa lược đồ (Schema Definition).

Hãy thực hiện một query trong giao diện GraphiQL để liệt kê các tác giả và trích dẫn của họ. Viết query sau vào giao diện và thực hiện nó.

{
  authors {
    id
    name
    quotes {
      text
      category
    }
  }
}

Sau đây là ảnh chụp màn hình đầu ra của truy vấn.

GraphQL query
GraphQL query

Bây giờ chúng ta hãy thực hiện một mutation để thêm một trích dẫn vào bản ghi tác giả và cũng liệt kê tất cả các trích dẫn của tác giả trong cùng một thao tác.

mutation {
  createQuote(
    quote: {
      authorId: 2
      text: "Pleasure in the job puts perfection in the work."
      category: "job"
    }
  ) {
    name
    quotes {
      text
      category
    }
  }
}

Sau đây là đầu ra của thao tác mutation. Lưu ý rằng trích dẫn mới được thêm vào cũng được trả lại trong phản hồi.

GraphQL mutation
GraphQL mutation

Có một query khác mà chúng ta đã tạo trong ứng dụng của mình để liệt kê chi tiết tác giả dựa trên id của tác giả. Tôi sẽ giao nó cho bạn thực hiện nó và kiểm tra kết quả.

Phần kết luận

Tôi hy vọng bạn thích làm việc thông qua mẫu và cũng đã học được những kiến ​​thức cơ bản về GraphQL. Trong phần tiếp theo, chúng ta sẽ thêm giao diện người dùng dựa trên TypeScript vào API để thực thi các truy vấn trên API GraphQL.

Xây dựng ứng dụng GraphQL với ASP.Net Core và TypeScript (P2)
Trong hướng dẫn này, chúng ta sẽ tìm hiểu cách xây dựng một ứng dụng khách bằng TypeScript để thực hiện các thao tác query và mutation với API GraphQL.
GraphQLASP.NET Core Web APITypeScriptASP.NET CoreEntity Framework Core
Bài Viết Liên Quan:
Xây dựng GraphQL cơ bản với ASP.NET Core và Entity Framework Core
Trung Nguyen 01/05/2021
Xây dựng GraphQL cơ bản với ASP.NET Core và Entity Framework Core

Hướng dẫn bạn các bước để tạo một API GraphQL trên ASP.NET Core bằng GraphQL, Entity Framework Core, Autofac và mẫu Repository.

Xây dựng ứng dụng GraphQL với ASP.Net Core và TypeScript (P2)
Trung Nguyen 27/04/2021
Xây dựng ứng dụng GraphQL với ASP.Net Core và TypeScript (P2)

Trong hướng dẫn này, chúng ta sẽ tìm hiểu cách xây dựng một ứng dụng khách bằng TypeScript để thực hiện các thao tác query và mutation với API GraphQL.

GraphQL + NodeJS: Giới thiệu
Trung Nguyen 16/05/2020
GraphQL + NodeJS: Giới thiệu

Giới thiệu hướng dẫn xây dựng API sử dụng GraphQL và NodeJS. Các mục tiêu của hướng dẫn và mã nguồn.

Các kiến trúc sử dụng GraphQL
Trung Nguyen 15/05/2020
Các kiến trúc sử dụng GraphQL

Hướng dẫn này sẽ trình bày qua 3 loại kiến ​​trúc khác nhau sử dụng máy chủ GraphQL.