Xây dựng GraphQL cơ bản với ASP.NET Core và Entity Framework Core

Xây dựng GraphQL cơ bản với ASP.NET Core và Entity Framework Core

Bài viết này sẽ hướng dẫn bạn các bước để tạo một API GraphQL cơ bản trên ASP.NET Core bằng GraphQL cho .NET, Entity Framework Core, Autofac và mẫu thiết kế Repository. Tôi đã chọn ngăn xếp công nghệ cho ứng dụng mẫu dựa trên mức độ phổ biến của các framework và mẫu. Bạn có thể thay thế các framework hoặc thư viện bằng các thành phần tương đương trong quá trình triển khai của mình.

Nếu bạn chưa quen với các khái niệm về GraphQL, hãy dành chút thời gian để đọc loạt bài viết tìm hiểu trên trang web GraphQL. Bây giờ hãy mở trình soạn thảo hoặc IDE ưa thích của chúng ta để bắt đầu.

Ứng dụng: Movie Reviews

Chúng ta sẽ tạo một API giới thiệu một bộ phim và các bài đánh giá về nó. Trong GraphQL, các query được sử dụng để đọc dữ liệu và các mutation được sử dụng để tạo, cập nhật và xóa dữ liệu. Để khám phá các hoạt động CRUD trên dữ liệu, chúng ta sẽ tạo hai hoạt động GraphQL như sau:

  1. Query tìm nạp một bộ phim bằng mã định danh của nó.
  2. Mutation để thêm đánh giá cho một bộ phim.

Tôi sẽ không đề cập đến GraphQL Subscriptions là một cách để tạo và duy trì kết nối thời gian thực với máy chủ GraphQL. Tính năng này cho phép máy chủ đẩy thông tin tức thì về các sự kiện liên quan đến máy khách. Bạn có thể đọc thêm về Subscriptions trên trang web tài liệu của Apollo.

Thiết lập dự án ASP.NET Core Web API

Tạo một dự án ASP.NET Core Web API có tên MovieReviews bằng Visual Studio hoặc lệnh sau:

dotnet new webapi -n MovieReviews

Hãy thêm các gói NuGet hỗ trợ cho GraphQL, Entity Framework Core và Autofac trong dự án của chúng ta. Để đơn giản, tôi sẽ sử dụng cơ sở dữ liệu trong bộ nhớ để duy trì dữ liệu phim. Bạn có thể sử dụng bất kỳ cơ sở dữ liệu nào được hỗ trợ bởi Entity Framework cho mục đích này.

dotnet add package GraphQL
dotnet add package GraphQL.SystemTextJson
dotnet add package GraphQL.Server.Transports.AspNetCore
dotnet add package GraphQL.Server.Ui.Altair

dotnet add package Autofac

dotnet add package Microsoft.EntityFrameworkCore
dotnet add package Microsoft.EntityFrameworkCore.InMemory

Bây giờ chúng ta sẽ cập nhật phương thức CreateHostBuilder trong lớp Program để sử dụng Autofac làm nhà cung cấp dịch vụ chịu trách nhiệm Dependency Injection (DI).

public static IHostBuilder CreateHostBuilder(string[] args)
{
    return Host.CreateDefaultBuilder(args)
        .UseServiceProviderFactory(new AutofacServiceProviderFactory())
        .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup<Startup>(); });
}

Tạo mô hình API

Bây giờ chúng ta sẽ xác định các mô hình / thực thể Movie và Review cho dự án của chúng ta. Tạo một thư mục có tên Models và tạo một tệp lớp có tên Review trong đó như sau:

public class Review
{
    public int Id { get; set; }
    public Guid MovieId { get; set; }
    public string Reviewer { get; set; }
    public int Stars { get; set; }
}

Tương tự, tạo một tệp lớp có tên Movie như sau:

public class Movie
{
    public IList<Review> Reviews { get; set; }
    public Guid Id { get; set; }
    public string Name { get; set; }

    public void AddReview(Review review)
    {
        Reviews.Add(review);
    }
}

Tiếp theo, chúng ta sẽ cấu hình Entity Framework Core để hoạt động với các thực thể này.

Thiết lập cơ sở dữ liệu

Bây giờ chúng ta sẽ thiết lập kết nối với cơ sở dữ liệu. Như ta đã nói trước đây, chúng ta sẽ sử dụng cơ sở dữ liệu InMemory với Entity Framework Core.

Tạo một thư mục có tên Database trong dự án và tạo một tệp lớp có tên MovieContext trong thư mục. Mã của MovieContext như sau:

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

    public DbSet<Movie> Movie { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Movie>().OwnsMany(m => m.Reviews).HasData(
            new Review
            {
                Id = 1,
                Reviewer = "A",
                Stars = 4,
                MovieId = new Guid("72d95bfd-1dac-4bc2-adc1-f28fd43777fd")
            },
            new Review
            {
                Id = 2,
                Reviewer = "B",
                Stars = 5,
                MovieId = new Guid("72d95bfd-1dac-4bc2-adc1-f28fd43777fd")
            },
            new Review
            {
                Id = 3,
                Reviewer = "A",
                Stars = 4,
                MovieId = new Guid("c32cc263-a7af-4fbd-99a0-aceb57c91f6b")
            },
            new Review
            {
                Id = 4,
                Reviewer = "D",
                Stars = 5,
                MovieId = new Guid("c32cc263-a7af-4fbd-99a0-aceb57c91f6b")
            },
            new Review
            {
                Id = 5,
                Reviewer = "E",
                Stars = 3,
                MovieId = new Guid("c32cc263-a7af-4fbd-99a0-aceb57c91f6b")
            },
            new Review
            {
                Id = 6,
                Reviewer = "F",
                Stars = 5,
                MovieId = new Guid("c32cc263-a7af-4fbd-99a0-aceb57c91f6b")
            },
            new Review
            {
                Id = 7,
                Reviewer = "A",
                Stars = 2,
                MovieId = new Guid("7b6bf2e3-5d91-4e75-b62f-7357079acc51")
            },
            new Review
            {
                Id = 8,
                Reviewer = "B",
                Stars = 1,
                MovieId = new Guid("7b6bf2e3-5d91-4e75-b62f-7357079acc51")
            },
            new Review
            {
                Id = 9,
                Reviewer = "G",
                Stars = 3,
                MovieId = new Guid("7b6bf2e3-5d91-4e75-b62f-7357079acc51")
            },
            new Review
            {
                Id = 10,
                Reviewer = "H",
                Stars = 4,
                MovieId = new Guid("7b6bf2e3-5d91-4e75-b62f-7357079acc51")
            }
        );
        modelBuilder.Entity<Movie>().HasData(
            new Movie
            {
                Id = new Guid("72d95bfd-1dac-4bc2-adc1-f28fd43777fd"),
                Name = "Superman and Lois"
            },
            new Movie
            {
                Id = new Guid("c32cc263-a7af-4fbd-99a0-aceb57c91f6b"),
                Name = "Game of Thrones"
            },
            new Movie
            {
                Id = new Guid("7b6bf2e3-5d91-4e75-b62f-7357079acc51"),
                Name = "Avengers: Endgame"
            }
        );
    }
}

Lưu ý rằng chúng ta đã thêm một số phim và đánh giá làm dữ liệu gốc trong cơ sở dữ liệu.

Bây giờ chúng ta sẽ đăng ký lớp DbContext của chúng ta với ứng dụng. Di chuyển đến lớp Startup và thêm mã sau vào phương thức ConfigureServices:

public void ConfigureServices(IServiceCollection services)
{
    services
        .AddEntityFrameworkInMemoryDatabase()
        .AddDbContext<MovieContext>(context => { context.UseInMemoryDatabase("MovieDb"); });
}

Đoạn mã trước đó hướng dẫn Entity Framework tạo cơ sở dữ liệu trong bộ nhớ có tên MovieDb. Cơ sở dữ liệu trong bộ nhớ rất tốt cho các bài kiểm tra và cơ sở dữ liệu trong bộ nhớ sẽ đủ để phục vụ ứng dụng demo của chúng ta.

Hãy triển khai mô hình kho lưu trữ sử dụng ngữ cảnh cơ sở dữ liệu để tìm nạp phim và thêm đánh giá phim. Tạo một interface có tên IMovieRepository trong thư mục Database như sau.

public interface IMovieRepository
{
    Task<Movie> GetMovieByIdAsync(Guid id);
    Task<Movie> AddReviewToMovieAsync(Guid id, Review review);
}

Hãy triển khai interface này trong một lớp có tên MovieRepository như sau:

public class MovieRepository : IMovieRepository
{
    private readonly MovieContext _context;

    public MovieRepository(MovieContext context)
    {
        _context = context;
        _context.Database.EnsureCreated();
    }

    public Task<Movie> GetMovieByIdAsync(Guid id)
    {
        return _context.Movie.Where(m => m.Id == id).AsNoTracking().FirstOrDefaultAsync();
    }

    public async Task<Movie> AddReviewToMovieAsync(Guid id, Review review)
    {
        var movie = await _context.Movie.Where(m => m.Id == id).FirstOrDefaultAsync();
        movie.AddReview(review);
        await _context.SaveChangesAsync();
        return movie;
    }
}

Chúng ta sẽ thiết lập dependency injection bằng cách sử dụng Autofac ở cuối, điều này sẽ làm cho mã injection trong phương thức khởi tạo hoạt động. Bây giờ hãy bắt đầu thêm hỗ trợ GraphQL vào API của chúng ta, bắt đầu với middleware GraphQL.

Middleware GraphQL

Chúng ta đã  thêm các gói NuGet GraphQL có liên quan vào ứng dụng của mình. Một trong những gói mà chúng ta đã cài đặt là gói GraphQL.Server.Ui.Altair cung cấp ứng dụng GraphQL UI giúp bạn gỡ lỗi các query và mutation GraphQL.

Chúng ta sẽ thêm giao diện người dùng Altair vào ứng dụng của mình bằng cách thêm middleware app.UseGraphQLAltair() trong phương thức Configure của lớp Startup như sau:

// Enables Altair UI at path /
app.UseGraphQLAltair(new GraphQLAltairOptions { Path = "/" });

Middleware sẽ cung cấp giao diện người dùng Altair tại điểm cuối (/) mặc định.

Hãy cấu hình các dịch vụ GraphQL được yêu cầu trong ứng dụng của chúng ta. Thêm mã sau vào phương thức ConfigureServices trong lớp Startup:

services.AddGraphQL((options, provider) =>
    {
        // Load GraphQL Server configurations
        var graphQLOptions = Configuration
            .GetSection("GraphQL")
            .Get<GraphQLOptions>();
        options.ComplexityConfiguration = graphQLOptions.ComplexityConfiguration;
        options.EnableMetrics = graphQLOptions.EnableMetrics;
        // Log errors
        var logger = provider.GetRequiredService<ILogger<Startup>>();
        options.UnhandledExceptionDelegate = ctx =>
            logger.LogError("{Error} occurred", ctx.OriginalException.Message);
    })
    // Adds all graph types in the current assembly with a singleton lifetime.
    .AddGraphTypes()
    // Add GraphQL data loader to reduce the number of calls to our repository. https://graphql-dotnet.github.io/docs/guides/dataloader/
    .AddDataLoader()
    .AddSystemTextJson();

GraphQL.NET SDK sử dụng mẫu builder để cấu hình các dịch vụ GraphQL được yêu cầu. Phương thức AddGraphQL cấu hình các thiết lập toàn cầu nhất định, chẳng hạn như độ sâu tối đa của một query. Chúng tôi cũng đã cấu hình trình ghi nhật ký để ghi lại bất kỳ ngoại lệ GraphQL nào chưa được xử lý.

Phương thức AddGraphTypes quét các assembly của ứng dụng để phát hiện các kiểu dữ liệu (schema, query và mutation) và đăng ký chúng trong container DI với lifetime là singleton.

Phương thức AddDataLoader tối ưu hóa các cuộc gọi đến kho lưu trữ của chúng ta để dữ liệu được phục vụ với càng ít yêu cầu cơ sở dữ liệu càng tốt. Bạn có thể đọc thêm về tính năng này trên tài liệu GraphQL.NET.

GraphQL.NET hỗ trợ JSON serialize các yêu cầu và phản hồi thông qua cả System.Text.JSON và Newtonsoft.JSON. Phương thức AddSystemTextJson chỉ thị nó sử dụng System.Text.JSON để serialize yêu cầu và phản hồi.

GraphQL Query

Chúng ta đã xác định MovieReview là các lớp mà chúng ta muốn thực hiện một query và mutation. Tuy nhiên, chúng ta không thể sử dụng trực tiếp một query hoặc một mutation trên các lớp này. Để làm cho các lớp này có khả năng query, chúng ta cần tạo một kiểu mới và kế thừa nó từ kiểu ObjectGraphType<T> với <T> là kiểu của đối tượng mà đồ thị thể hiện: Movie hoặc Review.

Tạo một thư mục có tên GraphQL và tạo một thư mục khác có tên là Types trong đó. Thêm tệp lớp MovieObject vào thư mục.

public sealed class MovieObject : ObjectGraphType<Movie>
{
    public MovieObject()
    {
        Name = nameof(Movie);
        Description = "A movie in the collection";

        Field(m => m.Id).Description("Identifier of the movie");
        Field(m => m.Name).Description("Name of the movie");
        Field(
            name: "Reviews",
            description: "Reviews of the movie",
            type: typeof(ListGraphType<ReviewObject>),
            resolve: m => m.Source.Reviews);
    }
}

Tiếp theo, thêm tệp lớp ReviewObject vào thư mục như sau:

public sealed class ReviewObject : ObjectGraphType<Review>
{
    public ReviewObject()
    {
        Name = nameof(Review);
        Description = "A review of the movie";

        Field(r => r.Reviewer).Description("Name of the reviewer");
        Field(r => r.Stars).Description("Star rating out of five");
    }
}

Tiếp theo, chúng ta sẽ tạo lược đồ GraphQL. Một lược đồ xác định API của máy chủ, thông báo cho máy khách về các hoạt động (query, mutation và subscription) mà máy chủ có thể thực hiện. Để xác định lược đồ của chúng ta, hãy tạo một tệp lớp có tên MovieReviewSchema trong thư mục GraphQL và mã của nó như sau:

public class MovieReviewSchema : Schema
{
    public MovieReviewSchema(QueryObject query, MutationObject mutation, IServiceProvider sp) : base(sp)
    {
        Query = query;
        Mutation = mutation;
    }
}

Chúng ta sẽ định nghĩa thao tác mutation ở phần sau. Hãy bắt đầu với việc định nghĩa query được đóng gói trong lớp QueryObject. Tạo một tệp lớp có tên QueryObject trong thư mục GraphQL như sau:

public class QueryObject : ObjectGraphType<object>
{
    public QueryObject(IMovieRepository repository)
    {
        Name = "Queries";
        Description = "The base query for all the entities in our object graph.";

        FieldAsync<MovieObject, Movie>(
            "movie",
            "Gets a movie by its unique identifier.",
            new QueryArguments(
                new QueryArgument<NonNullGraphType<IdGraphType>>
                {
                    Name = "id",
                    Description = "The unique GUID of the movie."
                }),
            context => repository.GetMovieByIdAsync(context.GetArgument("id", Guid.Empty)));
    }
}

Chúng ta đã định nghĩa một query có tên movie nhận id kiểu GUID làm đầu vào và trình giải quyết truy vấn sử dụng phương thức GetMovieByIdAsync của movie repository để tìm nạp đối tượng movie theo id.

GraphQL Mutation

Để định nghĩa mutation thêm đánh giá phim, hãy thêm một kiểu biểu đồ khác có tên ReviewInputObject trong thư mục Types với mã sau:

public sealed class ReviewInputObject : InputObjectGraphType<Review>
{
    public ReviewInputObject()
    {
        Name = "ReviewInput";
        Description = "A review of the movie";

        Field(r => r.Reviewer).Description("Name of the reviewer");
        Field(r => r.Stars).Description("Star rating out of five");
    }
}

Tiếp theo, hãy định nghĩa mutation được gói gọn trong kiểu đồ thị MutationObject. Tạo một tệp lớp có tên MutationObject trong thư mục GraphQL như sau:

public class MutationObject : ObjectGraphType<object>
{
    public MutationObject(IMovieRepository repository)
    {
        Name = "Mutations";
        Description = "The base mutation for all the entities in our object graph.";

        FieldAsync<MovieObject, Movie>(
            "addReview",
            "Add review to a movie.",
            new QueryArguments(
                new QueryArgument<NonNullGraphType<IdGraphType>>
                {
                    Name = "id",
                    Description = "The unique GUID of the movie."
                },
                new QueryArgument<NonNullGraphType<ReviewInputObject>>
                {
                    Name = "review",
                    Description = "Review for the movie."
                }),
            context =>
            {
                var id = context.GetArgument<Guid>("id");
                var review = context.GetArgument<Review>("review");
                return repository.AddReviewToMovieAsync(id, review);
            });
    }
}

Cuối cùng, chúng ta sẽ đăng ký lược đồ MovieReviewSchema bằng cách sử dụng middleware GraphQL. Vui lòng điều hướng đến phương thức Configure trong lớp Startup và thêm đoạn mã sau vào nó:

app.UseGraphQL<MovieReviewSchema>();

Bạn có thể nhận thấy rằng chúng ta chưa định nghĩa bất kỳ trình điều khiển API hoặc trình xử lý HTTP nào để xử lý một yêu cầu GraphQL đến. Trình xử lý tùy chỉnh không bắt buộc vì middleware GraphQL xử lý các yêu cầu HTTP đến điểm cuối GrapQL. Đường dẫn mặc định đến điểm cuối GraphQL là /graphql. Bạn có thể chỉ định một đường dẫn thay thế làm đối số cho phương thức UseGraphQL<T>.

Autofac Dependency Injection Container

Cuối cùng, hãy kết hợp mọi thứ với nhau bằng cách cấu hình Dependency Injection trong lớp Startup. Tạo một phương thức có tên ConfigureContainer, một phương thức đặc biệt được sử dụng để đăng ký phụ thuộc trực tiếp với Autofac. Bạn không cần phải xây dựng vùng chứa phụ thuộc vì nó được xây dựng tự động bởi thể hiện AutofacServiceProviderFactory được chỉ định trong lớp Program.

public virtual void ConfigureContainer(ContainerBuilder builder)
{
    builder.RegisterType<HttpContextAccessor>().As<IHttpContextAccessor>().SingleInstance();
    builder.RegisterType<MovieRepository>().As<IMovieRepository>().InstancePerLifetimeScope();
    builder.RegisterType<DocumentWriter>().AsImplementedInterfaces().SingleInstance();
    builder.RegisterType<QueryObject>().AsSelf().SingleInstance();
    builder.RegisterType<MovieReviewSchema>().AsSelf().SingleInstance();
}

Debug

Hãy khởi động máy chủ GraphQL và sử dụng Altair để gỡ lỗi API. Nhấn F5 hoặc sử dụng lệnh dotnet run. Điều hướng đến URL cơ sở của ứng dụng của bạn để mở giao diện người dùng Altair, như thể hiện trong hình bên dưới.

Altair UI
Altair UI

Hãy thử tìm nạp các chi tiết của một bộ phim bằng cách sử dụng query sau:

query {
  movie(id: "72d95bfd-1dac-4bc2-adc1-f28fd43777fd") {
    id
    name
    reviews {
      reviewer
      stars
    }
  }
}
Query Result
Query Result

Bây giờ chúng ta hãy thực hiện thao tác mutation sau để thêm đánh giá cho một bộ phim:

mutation addReview($review: ReviewInput!) {
  addReview(id: "72d95bfd-1dac-4bc2-adc1-f28fd43777fd", review: $review) {
    id
    name
    reviews {
      reviewer
      stars
    }
  }
}

Chúng ta yêu cầu một biến có tên review kiểu ReviewInput cho hoạt động này. Hãy khai báo biến như sau:

{
  "review": {
    "reviewer": "Rahul",
    "stars": 5
  }
}

Thực hiện thao tác để tìm nạp bản ghi phim đã cập nhật.

Mutation Result
Mutation Result

Phần kết luận

Trong bài viết này, tôi đã trình bày những kiến ​​thức cơ bản về thiết lập API GraphQL với ASP.NET Core. Chúng ta đã xác định lược đồ bằng cách sử dụng cách tiếp cận code first của Entity Framework Core, xác định các loại biểu đồ và viết trình xử lý query / mutation để thiết lập API.

Chúng ta đã sử dụng gói NuGet GraphQL cho .NET, gói này đã thực hiện công việc nặng nhọc với thiết lập tối thiểu. Chúng ta đã sử dụng Autofac và mẫu repository để tách các thành phần riêng lẻ và tuân thủ  Nguyên tắc đơn trách nhiệm (SRP). Cuối cùng, chúng ta đã sử dụng giao diện người dùng Altair để gỡ lỗi API.

Tôi khuyến khích bạn mở rộng các loại thao tác được hỗ trợ để xuất danh sách phim và khám phá các đăng ký để đưa các đánh giá mới đến khách hàng.

Trả lời

Email của bạn sẽ không được hiển thị công khai. Các trường bắt buộc được đánh dấu *