پیاده سازی GraphQl در Asp Core قسمت دوم

در مقاله قبلی به صورت کلی و خلاصه GraphQl را مورد بررسی قرار دادیم. در این مقاله با یک مثال ساده مفاهیم جلسه قبل را به صورت عملی پیاده سازی خواهیم کرد. ریپازیتوری پروژه را می توانید اینجا مشاهده کنید.

اگه این مقاله براتون میفید بود میتونید در کافیته یک قهوه مهمونم کنید : )
www.coffeete.ir/Sobhan

اگر هر داده ای اضافه بر مقاله دارید ممنون میشم تو قسمت کامنت ها بنویسید یا برام ایمیل کنید تا به مقاله اضافه کنم.

برای درک بهتر مقاله قبل ، قصد داریم یک پروژه ساده را با استفاده از GraphQl بالا بیاریم. برای پیاده سازی GraphQl محدود به زبان برنامه نویسی نمی باشید و با هر تکنولوژی که قبلا کار کردید می توانید پیاده سازی را انجام دهید حتی در انتخاب دیتابیس ها هم محدودیتی وجود ندارد و می توانید با sql ها یا nosql ها کار کنید. در اینجا می توان زبان برنامه نویسی مورد نظرتون رو انتخاب و شروع به کد نویسی کنید. ما در این مقاله از .net core استفاده خواهیم کرد برای اینکه در لینوکس هم محدودیت نداشته باشید با dotnet cli پروژه رو در محیط vs code ایجاد میکنیم. برای پیاده سازی از این مقاله استفاده میکنیم و سعی داریم در حد کامل تری مثال های بیشتری پیاده سازی کنیم.

توجه کنید مقاله معرفی شده با .NET 5 میباشد ولی ما از .NET 6 استفاده میکنیم پس یکسری تفاوت ها خواهد داشت


شروع به کار :

قبل از شروع پیش نیاز های لازم رو بر روی سیستم نصب کنید. پوشه جدیدی ساخته و با دستور زیر یک پروژه وب ایجاد و اجرا کنید.

dotnet new web
dotnet build

پس از ایجاد پروژه کتابخانه های زیر را نصب کنید و با دستور dotnet restore مطمئن شوید که پکیجا به درستی نصب شده اند

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 Microsoft.EntityFrameworkCore
dotnet add package Microsoft.EntityFrameworkCore.InMemory

درون root پروژه یک پوشه تحت عنوان Model ساخته و درون Model یک پوشه دیگر به نام Entity ایجاد میکنیم . درون Entity یک کلاس به نام Move ایجاد میکنیم

public class Movie
{
    public Guid Id { get; set; }
    public string Name { get; set; }
}

بعد از ساختن موجودیت ها سراغ تنظیمات مربوط به دیتابیس میرویم یک پوشه درون Model با نام Context ساخته و کلاس DbContext را ایجاد در ان ایجاد میکنیم

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>().HasData(
            new Movie
            {
                Id = new Guid(&quot72d95bfd-1dac-4bc2-adc1-f28fd43777fd&quot),
                Name = &quotSuperman and Lois&quot
            }
        );
    }
}

یکسری مقادیر پیش فرض را درون دیتابیس ثبت میکنیم. به کلاس Program.cs رفته و تنظیمات مربوط به کانکشن به دیتا بیس را انجام میدهیم

builder.Services
       .AddEntityFrameworkInMemoryDatabase()
       .AddDbContext<MovieContext>(context => { context.UseInMemoryDatabase(&quotMovieDb&quot); 
);

به Model باز گزشته و یک پوشه جدید به نام Repository ساخته و کلاس و اینترفیس IMovieRepository زیر را به ان اضافه کنید

public interface IMovieRepository{
    Task<Movie> GetMovieByIdAsync(Guid id);
    Task<Movie> AddMovieAsync(Guid id, Move move);
}


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> AddMoveMovieAsync(Guid id, Move move)
    {
        var movie = await _context.Movie.Where(m => m.Id == id).FirstOrDefaultAsync();
        movie.AddMove(move);
        await _context.SaveChangesAsync();
        return movie;
    }
}

در ریپازیتوری تعریف شده دو عمل ثبت و گرفتن بر روی دیتابیس انجام میدهیم . پس از تعریف ریپازیتوری باید ان را در Program.cs رجیستر کنیم

builder.Services.AddScoped<IMovieRepository, MovieRepository>();

تا اینجای کار روال عادی یک اپلیکیشن ساده رو پیش رفتیم و هنوز وارد پیاده سازی GraphQl نشده ایم.

تنظیمات مربوط به QrapgQL

برای شروع از Type ها شروع میکنیم .به root پروژه بازگشته و یک پوشه جدید به نام GraphQl ایجاد کنید. درون پوشه GraphQl یک کلاس به نام MovieType ساخته و به صورت زیر کانفیگ کنید

public class MovieType : ObjectGraphType<Movie>
    {
        public MovieType()
        {
            Name = &quotMovie&quot
            Description = &quotMovie Type&quot            
            Field(d => d.Id, nullable: false).Description(&quotMovie Id&quot);
            Field(d => d.Name, nullable: true).Description(&quotMovie Name&quot);
        }
    }

در مقاله قبل درباره Type ها در GraphQl به صورت خلاصه صحبت کردیم .کلاس MovieType از کلاس جنریک ObjectGraphType ارث بری کرده و دو فیلد Name و Description را می توانیم در کلاس فرزند مقدار دهی کنیم این مقادیر در schema ما نمایش داده میشود در ادامه فیلد های درون Movie رو به صورت دلخواه کانفیگ میکنیم .اگر با fluent validation اشنایی داشته باشید خیلی syntax مشابهی دارد.

بعد از ساختن Type باید کلاس Query مربوط به Movie را پیاده سازی کنیم .درون پوشه جاری کلاس MovieQuery را ایجاد میکنیم

 public class MovieQuery : ObjectGraphType
    {
        private readonly IMovieRepository _movieRepository;
        public MovieQuery(IMovieRepository movieRepository)
        {
            this._movieRepository = movieRepository;
            Name = &quotQuery&quot
            Field<ListGraphType<MovieType>>(&quotmovie&quot, &quotReturns a list of Movie&quot, resolve: context 
> _movieRepository.GetAll());
        }
    }

کلاس MovieQuery از ObjectGraphType ارث بری کرده و درون سازنده ان ریپازیتوری IMovieRepository تزریق کرده ایم . پراپرتی Name به ارث رسیده کلاس پدر را با مقدار دلخواه پر می کنیم .

توجه کنید توضیحات یا فیلد هایی که به صورت داکیومت پر میکند درschema نمایش داده میشد که باعث میشد استفاده کنندگان بتوانند راحت تر با کوئری ها کار کنند.

بعد از نوشتن Type ها و Queryها باید schema مربوطه را ایجاد کنیم .درون مسیر جاری کلاس MovieSchema را ایجاد کنید

    public class MovieSchema : Schema
    {
        public MovieSchema(IServiceProvider serviceProvider) : base(serviceProvider)
        {
            Query = serviceProvider.GetRequiredService<MovieQuery>();
        }
    }

کلاس تعریف شده از کلاس Schema ارث بری کرده که درون سازنده serviceProvider را به سازنده کلاس پدر ارسال میکنم همنچنین در ادامه MovieQuery تعریف شده را درون پراپرتی Query قرار میدهم با اینکار سرویس هارو رجیستر می کنیم.در مرحله اخر وارد program.cs شوید و به صورت زیر کانفیگ کنید

builder.Services.AddScoped<ISchema, MovieSchema>(services => new MovieSchema(new SelfActivatingServiceProvider(services)));
// register graphQL
builder.Services.AddGraphQL(options =>
{
    options.EnableMetrics = true;
})
                .AddSystemTextJson();

  app.UseGraphQLAltair();
app.UseGraphQL<ISchema>();

پروژه را اجرا کرده و وارد ادرس https://localhost:7000/ui/altair شوید.صفحه ای مطابق با شکل زیر مشاهده می کنید .این صفحه بسته به کتاب خانه یا تنظیماتی که انجام میشود می تواند ui مختلفی داشته باشد .بر روی داکیومت می توانید داکیومنت و Typeهایی که مقدار دهی کردیم را مشاهده کنید .کلید f12 را زده و وارد inspect شوید و بر روی تب network کلیک کنید .Query زیر را نوشته و برر روی send requst کلیک کنید.با توجه به مثال درون عکس فقط فیلد های درون Query در ریسپانس مشاده خواهند شد همچنین یک ابجکت به اسم extensions هم برگشت داده خواهد شد که شامل اطلاعات دقیق مروبوط به Query است.

صفحه زیر مشابه swagger در rest api است

برای درستی انجام کار می توانید خروجی را در تب network مشاهده کنید


تا اینجای کار زیر ساخت یک پروژه ساده GraphQl را ایجاد کرده ایم . کمی بیشتر مثال پیاده سازی خواهیم کرد.خیلی از مواقع بین موجودیت ها یا جداول دیتابیس روابط مختلفی داریم به طور مثال هر Movie می تواند چند تا Review داشته باشد که قصد داریم همراه با کوئری Movie لیست Review هم بتوانیم مشاهده کنیم.وارد پوشه Entity شوید و یک کلاس به نام Review به صورت زیر کنار Movie ایجاد کنید سپس تنظیمات مربوطه را انجام دهید.

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

به طور خلاصه ما یک جدول جدید به اسم Review به پروژه ایجاد کردیم درون DbContext تنظیمات مربوط به روابط بین جدل و مقدار دهی اولیه را انجام دادیم .یک Type جدید به اسم ReviewType ایجاد کردیم که درون MovieType از ان استفاده کردیم .کلاس MovieQuery را نیز بسته به نیاز تغییرات را اعمال کردیم .مجددا پروژه را اجرا کنید و کوئری زیر را به سرور ارسال کنید

query {
  movie(id: &quot72d95bfd-1dac-4bc2-adc1-f28fd43777fd&quot) {
    id
    name
    reviews {
      reviewer
      stars
    }
  }
}

این بار علاوه بر اینکه ورودی از کاربر گرفتیم ابجکت reviewer را نیز واکشی کرده ایم.

نکته خیلی مهم در قسمت واکشی دیتا این است که موقعی که شما کوئری از سمت GraphQL ارسال می کنید این کوئری در مرحله اخر وقتی که به دیتابیس ارسال میشود کلیه فیلد ها را بدون در نظر گرفتن ورودی کوئری GraphQL سلکت میکند! به طور مثال شما نیاز به دوتا فیلد name و id دارید وقتی این کوئری به سمت دیتابیس ارسال میشود ، کلیه فیلد ها درون دیتابیس واکشی می شوند سپس توسط GraphQL در ریسپانس فیلتر می شوند که ما فقط name و id را در خروجی مشاهده خواهیم کرد . برای رفع این مشکل می توانید از کتاب خانه HotChocolate.Data.EntityFramework استفاده کنید برای اطلاعات بیشتر این مقاله را مطالعه کنید

پیاده سازی Mutation

مثال های گفته شده تا الان برای واکشی دیتا بودن خیلی از مواقع پیش می ایند که داده ای درون سرور ویرایش کنیم(حذف ، اضافه ، ویرایش ) برای این کار از Mutation ها استفاده میکنیم.کلاس MutationMovie را به صورت زیر ساخته. همچنین یک کلاس دیگر ب اسم ReviewInputObject ایجاد کنید.این کلاس مثل dto ها عمل میکنند و فیلد هایی که نیاز داریم را در ان ایجاد میکنیم. به طور مثال نیاز نداریم که کاربران نام کاربری را ویرایش کنند.در مرحله اخر هم Mutation را در قسمت MovieSchema وارد کنید.

public class MutationObject : ObjectGraphType<object>
{
    public MutationObject(IMovieRepository repository)
    {
        Name = &quotMutations&quot
        Description = &quotThe base mutation for all the entities in our object graph.&quot
        FieldAsync<MovieObject, Movie>(
            &quotaddReview&quot,
            &quotAdd review to a movie.&quot,
            new QueryArguments(
                new QueryArgument<NonNullGraphType<IdGraphType>>
                {
                    Name = &quotid&quot,
                    Description = &quotThe unique GUID of the movie.&quot
                },
                new QueryArgument<NonNullGraphType<ReviewInputObject>>
                {
                    Name = &quotreview&quot,
                    Description = &quotReview for the movie.&quot
                }),
            context =>
            {
                var id = context.GetArgument<Guid>(&quotid&quot);
                var review = context.GetArgument<Review>(&quotreview&quot);
                return repository.AddReviewToMovieAsync(id, review);
            });
    }
}

پروژه را اجرا کرده و کوئری زیر را ارسال کنید

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

در قسمت VARIABLES داده زیر را هم ورارد کنید .این داده در واقع ورودی کوئری Mutation ما خواهد بود.

{  
&quotreview&quot: {
    &quotreviewer&quot: &quotRahul&quot,
    &quotstars&quot: 5
  }
}

با اجرا کوئری بالا داده های جدیدی درون دیتابیس ایجاد می شوند . عملیات های دیگر نیز خیلی شبیه به Query و Mutation میباشند وبا درک این دو مفهمون می توانید پیاده سازی کنید.


بسته به نیازمندی که در پروژه دارید پیاده سازی میتواند متفاوت باشد ولی GraphQl یک مفهموم مشخص است که در این مقاله سعی شد به صورت کاربردی بیان شود.

منابع

thecloudblog.net

dev.to