Merhaba arkadaşlar.
Bir süredir yeni bir makale yazmaya fırsat bulamadım. Hatta bu makalenin bir kısmını ise Ağustos ayında yazmıştım, fakat bir türlü tamamlayamamıştım. 🙂 Bulduğum ilk fırsatta ise tamamlamayı başardım.
Herneyse, sanırım GraphQL (ayrıca Asp.NET Core 2.0), veri erişimi ve sorgulama üzerine son dönemlerin en popüler konuları arasındadır. GraphQL özellikle günümüz çağında hızla gelişen bu business ihtiyaçlarına karşı, veri erişimini ve gelişmiş sorgulamasını client tarafına bırakarak, hızlı ve efficient bir şekilde development yapabilme imkanını biz developer’lara sunmaktadır.
Öncelikle GraphQL konusu, benim içinde yeni sayılabilecek bir konu. GraphQL ile henüz production üzerinde bir tecrübe edinemedim. Fakat yakın bir dönem içerisinde özellikle mobile API’larımız için bir gateway gibi GraphQL ile veri erişimi ve sorgulama işlemlerini gerçekleştirebilmeyi planlıyoruz. (Şuan üzerinde çalışıyoruz)
Bu makale içerisinde ise ASP.NET Core 2.0 ile GraphQL‘i kullanarak, sorgulama işlemlerini efficient bir şekilde nasıl gerçekleştirebiliriz konusuna, araştırmalarım sırasında edinebildiğim bilgiler doğrultusunda değinmeye çalışacağım.
Neden GraphQL sorusuna geçmeden önce, bir konuya değinmek istiyorum. Bir çok kişi REST‘in öleceği ve GraphQL‘in ise yeni gelecek olduğundan bahsediyor. Ben şahsen bu fikre katılmıyorum. REST ve GraphQL‘in birbirleri ile karıştırılmaması gerektiği düşüncesindeyim.
İlk olarak hem REST hem de GraphQL‘in bir arada kullanımı mümkündür. Ayrıca GraphQL doğru zaman ve doğru ihtiyaçlar karşısında kullanıldığında, veri erişimi (özellikle mobile tarafı için) ve querying konusunda client’lar için oldukça esneklik sağlamaktadır.
Peki, neden GraphQL kısmına şöyle bir giriş yapayım. Günümüz teknolojisinde mobile uygulamalarının kullanım oranının, oldukça fazla olduğu ortada. Çalışmış olduğum firmalarda, online kullanıcıların bir çoğunu neredeyse mobile kullanıcılar oluşturmakta. Development tarafına baktığımızda ise uygulamalarımızın building block’larını oluşturan API‘ları, hem web hemde mobile tarafları için kullanmaktayız.
İşte tam da bu noktada, özellikle mobile tarafı için bazı challenge’lar ile karşılaşmaktayız. Çünkü mobile tarafı için alınan data boyutu, pil ve şebeke kullanımı gibi durumlar büyük önem taşımaktadır. Genelde bu tarz challenge’ları, API‘ların önüne Gateway API‘lar yazarak aggregation işlemlerini gateway içerisinde gerçekleştirip, developer’ın istediği data’ları filtreleyerek alabilmesi ile atlatırız. Kulağa hoş gelse de, ne yazık ki iş burada bitmiyor. 🙂 Bununla birlikte versiyonlama ve backward compatibility gibi vb. challenge’larla da karşılaşmak durumunda kalabiliyoruz.
İşte bu gibi durumlar karşısında GraphQL‘in bizlere sunduğu aşağıdaki gibi bazı avantajlar bulunmaktadır.
Hızlı ve kolay development konusundan biraz bahsetmek gerekirse eğer, bir çok kurum içerisinde development ekipleri küçük domain’lere göre bölünüp, her ekip kendi sorumluluğunun bulunduğu uygulamaları geliştirmektedir. Benim çalıştığım bir kaç firmada bu şekildeydi.
Düşünelim, ürün detay sayfasında “1” numaralı ürünün detay bilgilerine ihtiyacımız var. Bir RESTful API içerisinde normal şartlarda bu bilgileri “api/products/1” şeklinde bir endpoint’e GET isteğinde bulunarak, kolaylıkla erişebiliriz. Mobile ekibin ise bu ürün bilgilerinden sadece “name“, “description” ve “price” field’larına ihtiyacı olup, bunlara ek olarak birde ürünün kategori bilgilerine de ihtiyacı olduğunu varsayalım.
Bu tarz durumlar karşısında, yapabileceğimiz iki farklı seçenek bulunmaktadır.
Bunlara ek olarak birde her ekibin kendi sprint’lerini koştuğunu varsayarsak, mobile ekip bu noktada “Product” API‘ından sorumlu ekibin ilgili development’ı yapmasını da bekleyecektir. Hızla yeni feature’lar eklemek istediğimiz bu teknoloji pazarında ise, beklemek çok da hoş olmayacaktır, değil mi?
GraphQL ile bir API tasarlandığında ise, mobile ekip diğer ekiplere bağımlı olmadan istediği data’yı tek bir request ile efficient bir şekilde sorgulayarak alabilmesi, hızlı bir şekilde ihtiyaçlarına göre development işlemlerini gerçekleştirebilmesi kolay bir hale gelmektedir.
Örnek bir GraphQL query’sine bakmak istersek eğer:
{ product(id: 1){ name description price category{ id name } } }
Yukarıdaki gibi single bir query ile istenilen data, client-driven generated bir şekilde kolaylıkla alınabilmektedir.
NOT: Bu işlemleri elbette bir RESTful API ile de yapabilmek mümkündür. (Querying, filtering, sorting, vb…) Fakat her yeni bir ihtiyaç karşısında istenilen feature’ın, RESTful API‘a implemente edilmesi veya client tarafında handle edilmesi gerekmektedir. (Roundtrips!) GraphQL bu noktada ön plana çıkarken, unutmamalıyız ki GraphQL‘in istenilenleri gerçekleştirebilmesi için ise, doğru bir mapping yapısına da sahip olması gerekmektedir.
Implementasyon aşamasında ise, örnek olarak basit bir “ürün” ve “kategori” içeren bir API tasarlayacağız.
Bunun için öncelikle aşağıdaki komut satırı ile yeni bir ASP.NET Core “webapi” projesi oluşturalım.
mkdir aspnetcoregarphql cd aspnetcoregarphql dotnet new webapi
Projenin oluşturulmasından sonra “Models” isminde bir klasör oluşturarak, “Category” ve “Product” model’lerini burada tanımlayalım.
namespace aspnetcoregraphql.Models { public class Category { public int Id { get; set; } public string Name { get; set; } List<Product> Products { get; set; } } }
namespace aspnetcoregraphql.Models { public class Product { public int Id { get; set; } public int CategoryId { get; set; } public string Name { get; set; } public string Description { get; set; } public double Price { get; set; } Category Category { get; set;} } }
Modelleri tanımlamanın ardından “Data” isimli bir klasör daha oluşturalım. İçerisinde ise “ICategoryRepository” ve “IProductRepository” interface’lerini aşağıdaki gibi oluşturalım.
namespace aspnetcoregraphql.Data { public interface ICategoryRepository { Task<List<Category>> CategoriesAsync(); Task<Category> GetCategoryAsync(int id); } }
namespace aspnetcoregraphql.Data { public interface IProductRepository { Task<List<Product>> GetProductsAsync(); Task<List<Product>> GetProductsWithByCategoryIdAsync(int categoryId); Task<Product> GetProductAsync(int id); } }
Bu interface’lerin implementasyonlarını ise, test data’ları ile birlikte aşağıdaki gibi gerçekleştirelim.
namespace aspnetcoregraphql.Data { public class CategoryRepository : ICategoryRepository { private List<Category> _categories; public CategoryRepository() { _categories = new List<Category>{ new Category() { Id = 1, Name = "Computers" }, new Category() { Id = 2, Name = "Mobile Phones" } }; } public Task<List<Category>> CategoriesAsync() { return Task.FromResult(_categories); } public Task<Category> GetCategoryAsync(int id) { return Task.FromResult(_categories.FirstOrDefault(x => x.Id == id)); } } }
namespace aspnetcoregraphql.Data { public class ProductRepository : IProductRepository { private List<Product> _products; public ProductRepository() { _products = new List<Product>{ new Product() { Id = 1, CategoryId = 1, Name = "Apple Macbook Pro 2016", Description = "Touchbar, 3.2GHZ", Price = 5000 }, new Product() { Id = 2, CategoryId = 1, Name = "Apple Macbook Air", Description = "2.3GHZ 8GB RAM", Price = 2000 }, new Product() { Id = 3, CategoryId = 1, Name = "Dell XPS 13", Description = "3.3GHZ 12GB RAM", Price = 4000 } }; } public Task<Product> GetProductAsync(int id) { return Task.FromResult(_products.FirstOrDefault(x => x.Id == id)); } public Task<List<Product>> GetProductsAsync() { return Task.FromResult(_products); } public Task<List<Product>> GetProductsWithByCategoryIdAsync(int categoryId) { return Task.FromResult(_products.Where(x => x.CategoryId == categoryId).ToList()); } } }
Model ve repository’ler hazır olduğuna göre, artık GraphQL‘in .NET client’ını implemente etmeye başlayabiliriz.
Bunun için öncelikle aşağıdaki komutu kullanarak, projemize GraphQL paketini NuGet üzerinden dahil edelim.
dotnet add package GraphQL
Paketi projemize ekledikten sonra, ilk olarak GraphQL Object Type‘larını tanımlamaya başlayalım. Object type‘ları ve field’lar GraphQL schema’sının en temel bileşenlerini oluşturmaktadır. Yani, oluşturacağımız servis üzerinden hangi objeleri alabileceğimizi ve hangi field’lara sahip olduğunu belirtmektedir.
Bu ön bilginin ardından “Models” klasörü içerisinde, “CategoryType” ve “ProductType” class’larını da aşağıdaki gibi oluşturalım.
namespace aspnetcoregraphql.Models { public class CategoryType : ObjectGraphType<Category> { public CategoryType(IProductRepository productRepository) { Field(x => x.Id).Description("Category id."); Field(x => x.Name, nullable: true).Description("Category name."); Field<ListGraphType<ProductType>>( "products", resolve: context => productRepository.GetProductsWithByCategoryIdAsync(context.Source.Id).Result.ToList() ); } } }
namespace aspnetcoregraphql.Models { public class ProductType : ObjectGraphType<Product> { public ProductType(ICategoryRepository categoryRepository) { Field(x => x.Id).Description("Product id."); Field(x => x.Name).Description("Product name."); Field(x => x.Description, nullable: true).Description("Product description."); Field(x => x.Price).Description("Product price."); Field<CategoryType>( "category", resolve: context => categoryRepository.GetCategoryAsync(context.Source.CategoryId).Result ); } } }
Dikkat edersek her iki class da, “ObjectGraphType” class’ından türetilmektedir. Sonrasında ise constructor içerisinden GraphQL paketinin fluent method’larını kullanarak, hangi field’lara sahip olması gerektiğini expression’lar ile tanımladık. Ardından relation field’ları için ise resolve function’larını yazdık.
Şimdi query işlemleri için kullanacak olduğumuz root type’ı oluşturacağız. Bunun için “EasyStoreQuery” isminde yeni bir class tanımlayalım ve aşağıdaki gibi kodlayalım.
namespace aspnetcoregraphql.Models { public class EasyStoreQuery : ObjectGraphType { public EasyStoreQuery(ICategoryRepository categoryRepository, IProductRepository productRepository) { Field<CategoryType>( "category", arguments: new QueryArguments( new QueryArgument<NonNullGraphType<IntGraphType>> {Name = "id", Description = "Category id"} ), resolve: context => categoryRepository.GetCategoryAsync(context.GetArgument<int>("id")).Result ); Field<ProductType>( "product", arguments: new QueryArguments( new QueryArgument<NonNullGraphType<IntGraphType>> {Name = "id", Description = "Product id"} ), resolve: context => productRepository.GetProductAsync(context.GetArgument<int>("id")).Result ); } } }
“EasyStoreQuery” root type’ı içerisinde, “CategoryType” ve “ProductType” da olduğu gibi constructor içerisinde schema’yı configure ettik. Configure işlemi sırasında “arguments” parametresi ile ise, ilgili field’ın hangi argument’ler doğrultusunda alınabileceğini belirttik.
NOT: “CategoryType“, “ProductType” ve “EasyStoreQuery” içerisindeki resolve function’larının source’u olarak “category” ve “product” repository’lerini configure etmek yerine, “category” ve ya “product” için olan ilgili REST endpoint’lerini de configure edebilirdik.
Artık dışarıya expose edecek olduğumuz data’mızın, structure’ını tanımlayacak olan schema’yı oluşturabiliriz. Bunun için “EasyStoreSchema” adında bir class daha oluşturalım ve “Schema” class’ından türeterek, aşağıdaki gibi kodlayalım.
namespace aspnetcoregraphql.Models { public class EasyStoreSchema : Schema { public EasyStoreSchema(Func<Type, GraphType> resolveType) :base(resolveType) { Query = (EasyStoreQuery)resolveType(typeof(EasyStoreQuery)); } } }
Bir schema’nın sahip olabileceği iki adet temel type bulunmaktadır. Bunlardan ilki “Query”, diğeri ise “Mutation” type’ıdır. Biz ise burada sadece “Query” type’ını kullandık. “EasyStoreSchema” class’ını DI container üzerinden inject ederken, resolve type olarak ise “EasyStoreQuery” type’ını parametre olarak geçeceğiz.
Artık geriye yapmamız gereken iki şey kaldı. Bunlardan ilki GraphQL endpoint’ini hazırlamak, bir diğeri de service injection işlemlerini gerçekleştirmek. Endpoint’i hazırlamak için öncelikle “Models” klasörü içerisinde “GraphQLQuery” isminde bir request class’ı oluşturalım ve aşağıdaki gibi tanımlayalım.
namespace aspnetcoregraphql.Models { public class GraphQLQuery { public string OperationName { get; set; } public string NamedQuery { get; set; } public string Query { get; set; } public string Variables { get; set; } } }
Request objesini tanımlamanın ardından, “Controllers” klasörü içerisinde ise “GraphQLController” isminde bir controller class’ı ekleyerek kodlamaya başlayalım.
namespace aspnetcoregraphql.Controllers { [Route("graphql")] public class GraphQLController : Controller { private readonly IDocumentExecuter _documentExecuter; private readonly ISchema _schema; public GraphQLController(IDocumentExecuter documentExecuter,ISchema schema) { _documentExecuter = documentExecuter; _schema = schema; } [HttpPost] public async Task<IActionResult> Post([FromBody]GraphQLQuery query) { if (query == null) { throw new ArgumentNullException(nameof(query)); } var executionOptions = new ExecutionOptions { Schema = _schema, Query = query.Query }; try { var result = await _documentExecuter.ExecuteAsync(executionOptions).ConfigureAwait(false); if (result.Errors?.Count > 0) { return BadRequest(result); } return Ok(result); } catch(Exception ex) { return BadRequest(ex); } } } }
İlk olarak “Route” attribute’ü ile endpoint’i belirledik ve ardından “IDocumentExecuter” ve “ISchema” interface’lerinin inject işlemlerini gerçekleştirdik. Bu noktada “IDocumentExecuter“, “ExecutionOptions” parametresini execute ederek, client’ın istemiş olduğu data’yı oluşturacaktır. Bu noktada, client-driven application development yapabilmek, çok da hoş değil mi?
Artık geriye sadece dependency injection işlemi kaldı. “Startup” class’ı içerisinde injection işlemlerini aşağıdaki gibi gerçekleştirelim.
public void ConfigureServices(IServiceCollection services) { services.AddMvc(); services.AddScoped<EasyStoreQuery>(); services.AddTransient<ICategoryRepository, CategoryRepository>(); services.AddTransient<IProductRepository, ProductRepository>(); services.AddScoped<IDocumentExecuter, DocumentExecuter>(); services.AddTransient<CategoryType>(); services.AddTransient<ProductType>(); var sp = services.BuildServiceProvider(); services.AddScoped<ISchema>(_ => new EasyStoreSchema(type => (GraphType) sp.GetService(type)) {Query = sp.GetService<EasyStoreQuery>()}); }
İşte hepsi bu kadar.
Şimdi terminal üzerinden “dotnet run” komutunu çalıştırarak, bir kaç test yapabiliriz.
NOT: Ben test işlemleri için “Postman” kullanacağım.
Aşağıdaki sorguyu GraphQL endpoint’ine, POST olarak gönderelim ve sonucunu görelim.
{ "query": "query{ category(id:1){ id name } }" }
Response’a baktığımızda ise aşağıda gördüğümüz gibi, “1” numaralı kategoriye ait “id” ve “name” field’ları gelmiştir.
{ "query": "query{ category(id:1){ id name products{ id name price } } }" }
Sonucu ise aşağıdaki gibi ortada.
GraphQL ile PoC yaparken gerçekten çok keyif aldım. Bu konudaki benim fikrim ise, eğer database schema’mız ve ya API design’larımız resource olarak kullanılmaya uygun ise, GraphQL’i implemente etmek özellikle client-driven application development için doğru bir tercih olacaktır. Ayrıca GraphQL client’a sadece istediği ölçüde data’yı verebilmesiyle beraber, server ile client arasındaki veri boyutunun küçülmesini de sağlamaktadır.
Örnek projeye ise buradan erişebilirsiniz: https://github.com/GokGokalp/ASP.NET-Core-2.0-GraphQL-Sample
https://github.com/graphql-dotnet/graphql-dotnet
http://graphql.org/learn/
{:tr} Makalenin ilk bölümünde, Software Supply Chain güvenliğinin öneminden ve containerized uygulamaların güvenlik risklerini azaltabilmek…
{:tr}Bildiğimiz gibi modern yazılım geliştirme ortamında containerization'ın benimsenmesi, uygulamaların oluşturulma ve dağıtılma şekillerini oldukça değiştirdi.…
{:tr}Bildiğimiz gibi bir ürün geliştirirken olabildiğince farklı cloud çözümlerinden faydalanmak, harcanacak zaman ve karmaşıklığın yanı…
{:tr}Bazen bazı senaryolar vardır karmaşıklığını veya eksi yanlarını bildiğimiz halde implemente etmekten kaçamadığımız veya implemente…
{:tr}Bildiğimiz gibi microservice architecture'ına adapte olmanın bir çok artı noktası olduğu gibi, maalesef getirdiği bazı…
{:tr}Bir önceki makale serisinde Dapr projesinden ve faydalarından bahsedip, local ortamda self-hosted mode olarak .NET…
View Comments
Güzel paylaştım. Teşekkürler
Teşekkür ederim.
Paylaşımız için teşekkür ederim ama kafamda oturmayan şeyler var :)
GraphQL kullanarak web service üzerinde herhangi bir değişiklik yapmadan hızlıca bir query oluşturup istediğimiz columnları tanımlayarak data getirme işlemleri yapabiliyoruz, bu işlem bize ortam bazında avantajlar sağlıyor(response data size or extra columns).
Peki burada bir spesifik bir filter vermek istediğimizde web service burada nasıl davranıyor. Örneğin şurada bir örnek var; https://www.howtographql.com/graphql-js/10-filtering/
Queryde belirttiğimiz filter tanımları bir repository aracılığıyla db seviyesindemi filter yapıyor yoksa repositoryde örneğin GetProducts() methodundan dönen liste içerisindemi bir filter uyguluyor?
Bu konuda biraz detay verebilir misiniz? Artı ve eksileri varmıdır?
Merhaba, teşekkür ederim öncelikle yorumunuz için. Bu işlemler aslında sizin ilgili source'u ve query argument'ları nasıl tanımladığınıza göre biraz değişiklik gösteriyor. Hatta relation'ların resolve kısımlarına yönelik bunun üzerine tartışılmış bir çok N+1 query sorusu, query optimizasyon vb. gibi çözümleri de mevcut. (Benim gördüğüm) Benim yapmış olduğum basit örnekte "id" argument'ı ile ilerlediğim için bir filtering söz konusu değil db tarafında, in-memory gerçekleşmektedir. Buradaki query argument'larını çoğaltabilir ve ya kendi query argument'larımız için graph type'lar oluşturabilir ve spesifik filter query'leri geçebilerek, predicate'ler ile de bir sorgu oluşturabiliriz db'ye hit etmeden önce veya ilgili servis'in bir filter endpoint'i varsa. Bu kullanmış olduğum .NET client'ı hala geliştirilmekte olan bir paket, ayrıca benim için de baya yeni bir konu süredir üzerinde çalışıyorum bu tarz concern'lere karşı, deneyimlerimi buradan paylaşmaya devam edeceğim.
Terimler arasında kaybolup gittim ama konuyla ilgili yeni makalelerinizi merakla bekliyorum :)
Ama özetle istemciden sunucudaki ilgili endpointe gelen request içerisindeki filtreyi bir convert işlemiyle ilgili orm tool yada data çekmek için kullandığımız data toolu ne ise, onun filter yapısına uydurup data çekmek işi çözecektir diye anlıyorum. Tabi bu convert işlemindeki pradicateleri uyumlu bir şekilde otomatik çeviren bir modül yoksa durum biraz vahim olabilir.
Merhaba tekrar, evet. :) Örnek vermek gerekirse eğer: "EasyStoreQuery" içerisindeki argument'lere bakarsak, "new QueryArgument {Name = "id", Description = "Category id"}" şeklinde bir id parametresi ekliyoruz aslında. GraphQL'in .NET client'ı ile buraları şekillendirmek bize kalıyor biraz. :) Bende üzerinde çalışmaktayım halen, ilerleyen bölümlerde paylaşmaya devam edeceğim edinebildiğim farklı tecrübeleri. :)
Paylaşım için teşekkürler güzel açıklama. Ben yinede grapql in neden kullanılması gerektiğini tam anlayamadım. Linq le çekip filtring yapmamızdan bir farkı yok gibi görünnüyor. Belki burda linq expressioni direk mobilden geliyomuş gibi düşünebiliriz. Buda mobilci için backend geliştirme beklemesini azaltır gibi.
Thanks ! Great post. Exactly what I was looking for.
I'm glad to hear that :)
So how to return a list Products?
Hi Gin, What you mean by "a list Products"? We already return product list with the following query:
{
"query":
"query{
category(id:1){
id
name
products{
id
name
price
}
}
}"
}
If you want to return a product list without category, you should implement it in "EasyStoreQuery" query class without "id" parameter. Then you can be querying.
Great write up and project. Thank you for your time explaining this.
Thanks for your comment. :)
I just listened to .netrocks podcast on graphql where it was mentioned that graphql never returns any other status than 200. Can you confirm and if required correct the code for error handling?
Hi Hemant, yes. For error handling, you can add "statusCode" and "errorMessage" fields in your response classes. Can you check this article: https://medium.com/@mczachurski/graphql-error-handling-17979dc571da
Merhaba, Öncelikle makale için teşekkür ederiz. Bu projeyi asp.net web api frameworkü ile çalıştıramadım.
GraphQLController içerisinde Constructor içerisinde schema injection'ının nasıl yapmalıyız. Teşekkürler.
var schema = new Schema { Query = new EasyStoreSchema(new Func(CategoryType))};
Merhaba ne tarz bir hata alıyorsunuz? Injection derken? Tanımlamasını Startup'da şu şekilde gerçekleştirdik makale için: services.AddScoped(_ => new EasyStoreSchema(type => (GraphType) sp.GetService(type)) {Query = sp.GetService()});
Merhabalar, Hangi sorguyu çalıştırmak istersem istiyim aşağıdaki gibi hata alıyorum.
Sorgu :
{
"query":
"query{
category(id:1){
Id
name
}
}"
}
Aldığım hata :
{
"errors": [
{
"message": "Syntax Error GraphQL (2:2) Expected Name, found String \"query\"\n1: { \n2: \"query\":\n ^\n3: \"query{\n"
}
]
}
Bir sorun mu var acaba
Merhaba kusura bakmayın geç cevap için, spam yorumlar arasında kaybolmuş. Eğer herşeyi aynı yaptı iseniz, kullandığınız NuGet paket versiyonu, benim kullandığım ile aynı mı? Bir değişiklikler olmuş olabilir farklı versiyon ise.
Kod paylaşılan yerlerde "" karakterlerinde eksiklikler var.
Bilginize.
küçüktür veya büyüktür karakterlerinde
Merhaba evet, kullandığım editör'den kaynaklı. Bir çok kod'da > gibi semböller de otomatik siliniyor kullandığım multi-language editör'ünden kaynaklı. Çözemedim bir türlü.