GraphQLNr: 1 Co to jest GraphQL? Czyli jak można jeszcze lepiej zrobić Rest API? GraphQL został wymyślony aby osiągnąć lepszą elastyczność i efektywność pomiędzy interakcjami klient / serwer.

GraphQL w esencji jest językiem zapytań do twojego HTTP API. Warto zaznaczyć, że GraphQL jest tylko nakładką do twojego API, a raczej używając języka ASP.NET Core jest to Middleware.

To nie biblioteka, to nie produkt, to nie baza danych.

Ta nakładka jest alternatywą do tworzenia Rest API. Zobaczmy na rysunkach w czym jest problem i co rozwiązuje GraphQL.

GrapQL polega tym, że wysyłasz do klienta polecenie HTTP POST albo GET. W treści tego polecenia jest JSON, które informuje API co chcesz zrobić.

Czyli jeśli chcesz listę produktów to nie szukasz odpowiedniego adresu i metody w REST API. Zastanawiasz się jak napisać zapytanie w JSON aby tą listę produktów pobrać.

Jak działa GraphQL ?

Mając normalne REST API w ASP.NET CORE chcesz mieć wiele kontrolerów i wiele metod w nim które odpowiedzą odpowiednio na zapytania HTTP.

Łapiesz co chce pobrać klient. Analizujesz jego zapytanie i pobierasz z bazy danych encję np. przy użyciu Entity Frameworka. Później twoja encja jest mapowana na model albo na Data Transfer Object lub ViewModel.

Normalne REST API w ASP.NET CORE

REST API w ASP.NET CORE to prosta sztuka z mapowania adresu URL na poszczególną metodę w danym kontrolerze.

Natomiast mając GraphQL nie musisz mieć ViewModeli czy innych klas pośrednich. Masz natomiast Schema.

Ta Schema deklaruje do czego klient ma dostęp. Może niechesz aby pewne właściwości w twoim dokumencie nie były wystawione na świat. Nie ma problemu po prostu nie deklarujesz ich w Schema.

Schema także wie, jak pobrać dane np. używając Entity Frameworka.

 

GraphQL z jednym kontrolerem wyjaśnienie

W przypadku GraphQL zamiast wielu kontrolerów wystarczy ci tylko jeden, który będzie reagował na zapytania JSON.

Możesz nawet pójść krok dalej. Zamienić ten jeden kontroler na Middleware, który także przetwarza zapytania HTTP. Tutaj możesz przeczytać o tym, jak napisać Middleware, który łapie wyjątki w aplikacji lub obsługuje zapytania HTTP.

GraphQL  z Middleware wyjaśnienie

Mając jednak GraphQL zawsze będziesz miał zapytanie. Zapytanie te :

  • Definiuje co się stanie
    • Nie metoda HTTP czy adres
  • Samo zapytanie nie jest powiązane z metodami HTTP
  • W sumie GraphQL nawet nie jest powiązane z HTTP. Tylko w tym wypadku korzysta z HTTP do transportu.

Istnieje jednak wada użycia GraphQL. Zapytania HTTP normalnie mogą być zapisywane w cache do ponownego użytku. W tym wypadku nie możesz tego zrobić.

Tyle teorii. Napiszmy proste API GraphQL w .NET. Tak wygląda mój projekt

DragonShop w Visual Studio

Żyjąc z zasadą Clean Architecture swoją aplikację zacząłem pisać od klas domenowych.

DragonShop.Domain w Visual Studio

Budujemy API dla sklepu ze smokami. Potrzebujemy więc encję smoka.

public class Dragon
{
    public int Id { get; set; }

    [Column(TypeName = "decimal(18,2)")]
    public decimal Price { get; set; }

    public int Rating { get; set; }

    public DateTimeOffset IntroducedAt { get; set; }

    public bool Active { get; set; }

    public int CrewCapacity { get; set; }

    public string Description { get; set; }

    public double DiameterMeters { get; set; }

    public int DryMassKg { get; set; }

    public bool DoneFirstFlight { get; set; }

    public int Age { get; set; }

    public int OrbitDurationYears { get; set; }

    public string Name { get; set; }

    public WhatColors Color { get; set; }

    public WhatBreath Breath { get; set; }

    public double HeightInMeters { get; set; }
}

Mam także dwa typy wyliczeniowe określające jego typ oddechu oraz kolor.

public enum WhatBreath
{
    None,
    Fire,
    Ice,
    Thunder,
    Acid,
}
public enum WhatColors
{
    Red,
    Yellow,
    Gold,
    Orange,
    Blue,
    Green,
    Purple,
}

To wszystko, jeśli chodzi o projekt Domain. Teraz napiszę warstwę dostępu do danych. Ja postanowiłem skorzystać Entity Frameworka oraz z Bazy danych SQLite. Dlaczego SQLite? Chociażby dlatego, że jak pobierzesz ten kod to nie musisz instalować jakiegoś systemu bazo danowego, aby uruchomić aplikację. Co prawda się przyjęło, że każdy  programista .NET ma zainstalowany SQL Server to ja wole od tej praktyki uciec.

Jakie paczki będą potrzebne na w tym projekcie?

DragonShop.Infrastructure.Persitence w VisualStudio

Oczywiście musisz zainstalować paczki związane z Entity Frameworkiem. Jak i jego implementację dla bazy SQLite. Dodatkowo zainstalowałem paczkę Microsoft.EntityFrameworkCore.Tools , aby szybko utworzyć tabelkę na podstawie kodu.

Oto XML mojego projektu.

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>net5.0</TargetFramework>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.EntityFrameworkCore" Version="5.0.6" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.6" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="5.0.6">
      <PrivateAssets>all</PrivateAssets>
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
    </PackageReference>
  </ItemGroup>
  <ItemGroup>
    <ProjectReference Include="..\DragonShop.Domain\DragonShop.Domain.csproj" />
  </ItemGroup>
</Project>

Najpierw tworzy kontekst bazo danowy dla naszych smoków

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

    }
    public DbSet<Dragon> Dragons { get; set; }
}

Teraz możesz utworzyć repozytorium.

public class DragonRepository
{
    private readonly DragonShopDbContext _dbContext;

    public DragonRepository(DragonShopDbContext dbContext)
    {
        _dbContext = dbContext;
    }

    public Task<List<Dragon>> GetAll()
    {
        return _dbContext.Dragons.ToListAsync();
    }
}

Nasze repozytorium na razie będzie miało metodę, która pobierze wszystkie smoki z bazy.

Dodałem też klasę, która do bazy doda jednego smoka. Skorzystamy z niej przy pierwszy uruchomieniu.

public static class InitialData
{
    public static void Seed(this DragonShopDbContext dbContext)
    {
        if (!dbContext.Dragons.Any())
        {
            dbContext.Dragons.Add(new Dragon
            {
                Name = "John",
                Description = "Stron.",
                Price = 219.5m,
                Rating = 4,
                Color = WhatColors.Green,
                Age = 30,
                OrbitDurationYears = 20,
                DryMassKg = 4330,
                DiameterMeters = 100,
                Active = true,
                Breath = WhatBreath.None,
                CrewCapacity = 20,
                DoneFirstFlight = true,
                HeightInMeters = 30,
                IntroducedAt = DateTimeOffset.Now.AddMonths(-1)
            });



            dbContext.SaveChanges();
        }
    }
    }

Pozostało na napisać jeszcze instalator według wzorca IServiceCollection. Zrobiłem o nim osobny wpis.

public static class DragonShopInstaller
{
    public static IServiceCollection AddDragonPersitence(this IServiceCollection services,
        IConfiguration _config)
    {
        services.AddDbContext<DragonShopDbContext>(options =>
options.UseSqlite(_config["ConnectionStrings:DragonDB"]));
        services.AddScoped<DragonRepository>();

        return services;
    }
}

To tyle, jeśli chodzi o warstwę dostępu do danych. Teraz czas na nasze API w GraphQL.

DragonShop.API w VisualStudio

Do użycia GraphQL potrzebujemy następujących paczek

Do użycia GraphQL potrzebujemy następujących paczek

Oto kod XML projektu

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>net5.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <ProjectReference Include="..\DragonShop.Infrastructure.Persitence\DragonShop.Infrastructure.Persitence.csproj" />
    <PackageReference Include="GraphQL" Version="4.5.0" />
    <PackageReference Include="GraphQL.Server.Transports.AspNetCore" Version="5.0.2" />
    <PackageReference Include="GraphQL.Server.Transports.AspNetCore.NewtonsoftJson" Version="5.0.2" />
    <PackageReference Include="GraphQL.Server.Ui.Playground" Version="5.0.2" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.6">
      <PrivateAssets>all</PrivateAssets>
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
    </PackageReference>

  </ItemGroup>

</Project>

Na początku zdefiniujmy naszą Scheme. Pamiętasz to ona definiuje jakie zapytania można wykonać do naszego GraphQL API.

public class DragonSchema : Schema
{
    private readonly DragonShopDbContext _dbContext;

    public DragonSchema(DragonShopDbContext dbContext) : base()
    {
        _dbContext = dbContext;

        Query = new DragonQuery
           (new DragonRepository(_dbContext));
    }


}

Teraz napiszmy nasze zapytanie.

public class DragonQuery : ObjectGraphType
{
    public DragonQuery(DragonRepository productRepository)
    {
        Field<ListGraphType<DragonType>>(
            "dragons",
            resolve: context => productRepository.GetAll()
        );
    }
}

Mówimy tutaj, że dla JSON-a z głównym nagłówkiem "dragons" zwrócimy mu wszystkie smoki.

Gdy dostanie takie zapytanie to wtedy z repozytorium wywołamy metodę GetAll().

Pozostało nam jeszcze określić jakie pola mogą być zwracane z tego zapytania. Ta definicja jest w klasie DragonType.

public class DragonType : ObjectGraphType<Dragon>
{
    public DragonType()
    {
        Field(t => t.Id);
        Field(t => t.Name).Description("The name of the draon");
        Field(t => t.Description);
        Field(t => t.IntroducedAt).Description("When the dragon was first introduced in the catalog");
        Field(t => t.Price);
        Field(t => t.Rating).Description("The (max 5) star customer rating");
        Field<ColorDragonType>("Color", "The color of dragon");
        Field<BreathDragonType>("Breath", "The breath of dragon");
        //Field(t => t.Active);
        //Field(t => t.Age);
        //Field(t => t.CrewCapacity);
        //Field(t => t.DiameterMeters);
        //Field(t => t.DoneFirstFlight);
        //Field(t => t.DryMassKg);
        //Field(t => t.OrbitDurationYears);
        //Field(t => t.HeightInMeters);
    }
}

Kto powiedział, że musimy odsłonić wszystkie właściwości. Jak widzisz jest to jakaś alternatywa dla tworzenia swojego ViewModel lub DataTransferObject. Warto zaznaczyć, że dla typów wyliczeniowych trzeba stworzyć kolejny taki pod typ.

Dla typów wyliczeniowych masz klasę pomocniczą EnumerationGraphType, która przyspieszy Ci tłumaczenie tej wartości na określony wzór.

public class BreathDragonType : EnumerationGraphType<WhatBreath>
{
    public BreathDragonType()
    {
        Name = "BreathDragonType";
        Description = "The type of BreathDragon";
    }
}
public class ColorDragonType : EnumerationGraphType<WhatColors>
{
    public ColorDragonType()
    {
        Name = "ColorDragonType";

        Description = "The type of ColorDragon";
    }
}

To wszystko, jeśli chodzi o nasz GraphQL. Pozostały nam jeszcze elementy konfiguracyjne. Jak dodanie połączenia z bazą danych.

Do appsetting.json dodajemy  taki connection string.

{
  "ConnectionStrings": {
    "DragonDB": "DataSource=./DataBase/DragonDB.db"
  }
}

Korzystając z DB Browser for SQLite utworzyłem taką pustą bazę w projekcie. Chciałbym jednak mieć tabelkę dragons na podstawie mojego kodu encji. 

Na pomoc przychodzą komendy Entity Framework Tools. W oknie Package Manager Console wpisałem polecenie Add-Migration InitialCreate, aby zostały utworzenie definicję mojego pierwsze procesu tworzenia bazy danej.

Add-Migration I Update-Database

update-database wykonał tą migrację na mojej bazie i tak mam tabelkę Dragon na podstawie modelu encji w C#

SQLite Smoki w mojej bazie

Warto zaznaczyć, że te narzędzia oferuję wiele innych poleceń. Mógłby np. przy pomocy  "Script-Migration" utworzyć sobie zapytanie SQL, który utworzyłby mi taką tabelkę Dragons.

The following Entity Framework Core commands are available.

Cmdlet                      Description
--------------------------  ---------------------------------------------------
Add-Migration               Adds a new migration.

Drop-Database               Drops the database.

Get-DbContext               Gets information about a DbContext type.

Remove-Migration            Removes the last migration.

Scaffold-DbContext          Scaffolds a DbContext and entity types for a database.

Script-Migration            Generates a SQL script from migrations.

Update-Database             Updates the database to a specified migration.

Oto taki skrypt SQL.

CREATE TABLE IF NOT EXISTS "__EFMigrationsHistory" (
    "MigrationId" TEXT NOT NULL CONSTRAINT "PK___EFMigrationsHistory" PRIMARY KEY,
    "ProductVersion" TEXT NOT NULL
);

BEGIN TRANSACTION;

CREATE TABLE "Dragons" (
    "Id" INTEGER NOT NULL CONSTRAINT "PK_Dragons" PRIMARY KEY AUTOINCREMENT,
    "Price" decimal(18,2) NOT NULL,
    "Rating" INTEGER NOT NULL,
    "IntroducedAt" TEXT NOT NULL,
    "Active" INTEGER NOT NULL,
    "CrewCapacity" INTEGER NOT NULL,
    "Description" TEXT NULL,
    "DiameterMeters" REAL NOT NULL,
    "DryMassKg" INTEGER NOT NULL,
    "DoneFirstFlight" INTEGER NOT NULL,
    "Age" INTEGER NOT NULL,
    "OrbitDurationYears" INTEGER NOT NULL,
    "Name" TEXT NULL,
    "Color" INTEGER NOT NULL,
    "Breath" INTEGER NOT NULL,
    "HeightInMeters" REAL NOT NULL
);

INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
VALUES ('20210528081044_InitialCreate', '5.0.6');

COMMIT;

Gdy już mamy bazę danych z tabelką pozostała nam konfiguracja GraphQL. Korzystamy z naszego instalatora z warstwy dostępu do danych. Dodajemy DragonSchema do serwisów.

public void ConfigureServices(IServiceCollection services)
{
    services.AddDragonPersitence(Configuration);
    services.AddScoped<DragonSchema>();

    services.AddGraphQL(o => { })
     .AddGraphTypes(ServiceLifetime.Scoped)
     .AddNewtonsoftJson();

    // If using Kestrel:
    services.Configure<KestrelServerOptions>(options =>
    {
        options.AllowSynchronousIO = true;
    });

    // If using IIS:
    services.Configure<IISServerOptions>(options =>
    {
        options.AllowSynchronousIO = true;
    });
}

Później dodajemy usługę GraphQL mówimy tutaj, z jakiego deserializatora ma korzystać.

Na koniec musimy zezwolić na synchroniczne operację IO, bo bez tego nie uruchomi się konsola testowa GraphQL.

public void Configure(IApplicationBuilder app, DragonShopDbContext dbContext)
{
    app.UseGraphQL<DragonSchema>();
    app.UseGraphQLPlayground();
    dbContext.Seed();

}

Pora uruchomić GrapQLPlayground (testowe UI aby zobaczyć jak działa twoje API) w potoku Middleware aplikacji ASP.NET CORE i dodać naszego 1 smoka, jeśli tabelka jest pusta przez metodę Seed().

GrapQLPlayground  mojego GrapQL API

Jak wykonywać zapytania ? Pod przyciskiem Schema masz wszystkie pola, które możesz dodać do zapytania.

Schema DragonType DragonQuery

Dla zapytania :

{
  dragons {
    id,
    introducedAt,
    price
  }
}

Dostaniesz wszystkie smoki tylko z tymi polami. Dla tego zapytania dostaniesz wszystkie meta informacje o typach, które wystawia moje API GraphQL.

{
  __schema {
    types{
      name
    }
  }
}

Oto co zwróciło zapytanie :

{
  "data": {
    "__schema": {
      "types": [
        {
          "name": "__DirectiveLocation"
        },
        {
          "name": "__TypeKind"
        },
        {
          "name": "__EnumValue"
        },
        {
          "name": "String"
        },
        {
          "name": "Boolean"
        },
        {
          "name": "__Directive"
        },
        {
          "name": "__InputValue"
        },
        {
          "name": "__Type"
        },
        {
          "name": "__Field"
        },
        {
          "name": "__Schema"
        },
        {
          "name": "DragonQuery"
        },
        {
          "name": "DragonType"
        },
        {
          "name": "Int"
        },
        {
          "name": "DateTimeOffset"
        },
        {
          "name": "Decimal"
        },
        {
          "name": "ColorDragonType"
        },
        {
          "name": "BreathDragonType"
        },
        {
          "name": "Float"
        }
      ]
    }
  },
  "extensions": {}
}

To wszystko, jeśli chodzi on naszą prostą aplikację GraphQL. Jak widzisz zapytania mogą być ciekawą alternatywą do REST API. 

Rodzą się oczywiście pytania, jak takie API ulepszyć. Jak później takie API odpytać, korzystać z innej aplikacji .NET jak Blazor. Jak kasować,dodawać, modyfikować dane przez GraphQL?

Jak skorzystać z mechanizmu subskrypcji. O tym też warto coś napisać. Ty mi powiedzieć czy warto.