GraphQLNr: 1 Co to jest GraphQL? Czyli jak można jeszcze lepiej zrobić Rest API?
GraphQL został stworzony, aby osiągnąć większą elastyczność i efektywność w interakcjach klient-serwer.
GraphQL w istocie jest językiem zapytań dla twojego HTTP API. Warto zaznaczyć, że GraphQL jest tylko nakładką na twoje API, a raczej używając języka ASP.NET Core jest to middleware.
To nie jest biblioteka, produkt ani baza danych.
Ta nakładka jest alternatywą dla tworzenia REST API. Spójrzmy na rysunki, aby zobaczyć, w czym tkwi problem i co rozwiązuje GraphQL.
GraphQL polega na tym, że wysyłasz do serwera polecenie HTTP POST lub GET. W treści tego polecenia znajduje się JSON, które informuje API, co chcesz zrobić.
Czyli jeśli chcesz listę produktów, nie szukasz odpowiedniego adresu i metody w REST API. Zastanawiasz się, jak napisać zapytanie w JSON, aby pobrać tę listę produktów.
Mając normalne REST API w ASP.NET CORE, chcesz mieć wiele kontrolerów i wiele metod w nich, które odpowiedzą na zapytania HTTP.
Łapiesz to, 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, Data Transfer Object lub ViewModel.
REST API w ASP.NET CORE to prosta sztuka 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 schemat.
Ten schemat definiuje, do czego klient ma dostęp. Możesz nie chcieć, aby pewne właściwości w twoim dokumencie były wystawione na świat. Nie ma problemu, po prostu nie deklaruj ich w schemacie.
Schemat również wie, jak pobrać dane, np. używając Entity Frameworka.
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 również przetwarza zapytania HTTP. Tutaj możesz przeczytać o tym, jak napisać Middleware, który łapie wyjątki w aplikacji lub obsługuje zapytania HTTP.
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 pamięci podręcznej 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.
Żyjąc z zasadą Clean Architecture, swoją aplikację zacząłem pisać od klas domenowych.
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. Postanowiłem skorzystać z Entity Frameworka oraz z bazy danych SQLite. Dlaczego SQLite? Chociażby dlatego, że jak pobierzesz ten kod, to nie musisz instalować jakiegoś systemu bazodanowego, aby uruchomić aplikację. Co prawda się przyjęło, że każdy programista .NET ma zainstalowany SQL Server, ale ja wolę od tej praktyki uciec.
Jakie paczki będą potrzebne na w tym projekcie?
Oczywiście musisz zainstalować paczki związane z Entity Frameworkiem. Jak i jego implementację dla bazy SQLite. Dodatkowo zainstalowałem pakiet 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 pierwszym 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.
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 schemat. Pamiętasz, że on 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, 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 podtyp.
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 appsettings.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ła utworzona definicja mojego pierwszego procesu tworzenia bazy danych.
Update-Database wykonał tę migrację na mojej bazie i tak mam tabelkę Dragon na podstawie modelu encji w C#.
Warto zaznaczyć, że te narzędzia oferują wiele innych poleceń. Mógłbym np. przy pomocy "Script-Migration" utworzyć sobie zapytanie SQL, które utworzyłoby mi taką tabelkę Dragons.
Oto taki skrypt SQL:
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 operacje 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ć GraphQL Playground (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().
Jak wykonywać zapytania? Pod przyciskiem Schema masz wszystkie pola, które możesz dodać do zapytania.
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 o 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 powiedz, czy warto. warto.