GraphQL’i beğendin mi? OData’yı dene!

asp-net-core-api-odata

OData (Open Data Protocol) en basit haliyle veri kaynaklarımızı bir Restful Api üzerinde, Http aracılığıyla -bir url ile- sorgulamamıza imkan veren bir protokoldür. Bu protokol sayesinde verilerimiz üzerinde filtreleme (filter), seçme (projection), bağlı alanları getirme (expand), sayfalama (top, skip) gibi fonksiyonları çok kolay bir şekilde kullanabiliriz. Bu yazımızda Restful Api yeteneklerini arttıran Open Data standardını ASP.NET Core ile inceleyeceğiz.

Öncelikle OData‘nın kullanılmadığı bir proje düşünelim. Tüm verileri çektiğimiz bir Get() methodu olduğunu düşünelim, ileride yalnızca belirli alanların (ör.: yalnızca id ve name alanları) dönülmesi ,istenebilir, sayfalama veya çeşitli query filter işlemleri de gerçekleştirilmesi gerekebilir. Tüm bu ihtiyaçlara cevap verebilmek için farklı farklı endpointlerin yazılması gerekecek. OData kullandığımız takdirde biz bu işin yalnızca business logic tarafıyla ilgileniyor, gerisini OData’ya bırakıyoruz.

Uygulama

Yazıyı uzatıp şişirmemek adına projeye buradan erişebilirsiniz. İçerisinde Category, Manufacturer ve Vehicle varlıklarının olduğu bir Api projesidir.

Öncelikle gerekli olan Nuget paketini kurarak işleme başlayalım.

Install-Package Microsoft.AspNetCore.OData

Daha sonra Startup.cs dosyamızı açarak, ConfigureServices() methodunda OData’yı bir servis olarak ekleyeceğimizi bildirmeliyiz.

public void ConfigureServices(IServiceCollection services)
{
	// ...
	services.AddOData();
	// ...
}

Ardından Configure() methodu içerisinde middleware olarak ekleyip; hangi varlıklarımızı OData’ya açacağmızı belirtmeliyiz.

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
	// ...
	app.UseEndpoints(endpoints =>
	{
		endpoints.MapODataRoute("ODataRoute", "odata", GetEdmModel());

		endpoints.MapControllers();
	});
	// ...
}

private IEdmModel GetEdmModel()
{
	var builder = new ODataConventionModelBuilder();
	builder.EntitySet<Category>("Categories");
	builder.EntitySet<Manufacturer>("Manufacturers");
	builder.EntitySet<Vehicle>("Vehicles");

	return builder.GetEdmModel();
}

Buraya kadar yaptıklarımızı kısaca özetleyelim. MapODataRoute() methodu ile OData route’ini ODataRoute ismi, “odata” url ön ekiyle (prefix) kullanacağımızı bildiriyoruz. Model olarak gönderdiğimiz GetEdmModel() içerisinde ise; dışarı açacağımız EntiySet’leri bir isimle bildiriyoruz. Burada dikkat edilmesi gereken nokta bu isimler sonra oluşturacağımız controller ismi ile eşleşmesi gerektiğidir. (Ör.: Vehicles için VehiclesController, Manufacturers için ManufacturersController gibi)

İşlemleri tamamladıktan sonra Controller sınıflarımıza geçebiliriz. Şimdilik tüm controller sınıflarındaki kodlar aynı olacağı için yalnızca VehiclesController sınıfını paylaşıyorum.

public class VehiclesController : ODataController
{
	private readonly AppDbContext _dbContext;

	public CategoriesController(AppDbContext dbContext)
	{
		_dbContext = dbContext;
	}

	[EnableQuery]
	public IActionResult Get()
	{
		return Ok(_dbContext.Vehicles);
	}
}

Sınıflarımızı ControllerBase değil ODataController abstract sınıfından inherit ettiğimize ve Get() methodunu [EnableQuery] ile işaretlediğimize dikkat edin. Bu Attribute yardımıyla OData‘ya sorgulama özelliğinin çalışma zamanında aktif edildiğini söylüyoruz.

Daha ileriye gitmeden önce bir konuya açıklık getirmem gerekiyor. OData ile veri dönmeyi hedeflediğimiz endpointlerimizi IQueryable olarak dönmemiz gerekiyor. Aksi halde, gönderdiğimiz sorgular IEnumerable olarak ele alınıp önce verilerin tamamı çekilerek belleğe yazılacak ve ardından sorgulama işlemi gerçekleşecektir. Bu da büyük verilerin olduğu bir tabloda performans kaybına yol açacaktır. IQueryable döndüğümüz takdirde gönderilen sorgular işlenerek bir sql sorgucuğuna dönüştürülecek ve bu sayede yalnızca ihtiyaç duydumuz verilerle çalışmış olacağız.

OData Metadata

Sorgu muhattabının hangi EntitySet olduğunu ve bu EntitySet içeriğini Metadata özelliği sayesinde görebilir, bir nevi Swagger gibi düşünebiliriz. Bu özellik için $metadata anahtar kelimesi (keyword) kullanılır.

https://localhost:44357/odata adresine çağrı yapalım.

{
    "@odata.context": "https://localhost:44357/odata/$metadata",
    "value": [
        {
            "name": "Categories",
            "kind": "EntitySet",
            "url": "Categories"
        },
        {
            "name": "Manufacturers",
            "kind": "EntitySet",
            "url": "Manufacturers"
        },
        {
            "name": "Vehicles",
            "kind": "EntitySet",
            "url": "Vehicles"
        }
    ]
}

Gördüğünüz üzere dışarı açtığımız EntitySet’ler ile birlikte $metadata bilgisi de döndü. Metadata bilgisini elde etmek için bir çağrı daha yapalım.

https://localhost:44357/odata/$metadata adresine çağrı attığımız zaman aşağıdaki çıktıyı görmeniz gerekmektedir.

open-data-metadata
Open data metadata çıktısı

https://localhost:44357/odata/$metadata#Vehicles adresiyle kullandığımız bir EntitySet ve bağlı bulunan -navigation property- tüm EntitySet meta bilgilerini de elde edebiliriz.

Buraya kadar her şey tamam gibi, artık OData‘nın güçlü yanlarını keşfedelim.

OData Query Options

Operatörlere geçmeden önce nasıl veri çekeceğimize de hızlıca bakalım.

https://localhost:44357/odata/manufacturers çağrısıyla tüm Manufacturer bilgisine erişebiliriz.

odata-get-data

$select filtresi

Bu filtre yardımıyla belirttiğimiz EntitySet üzerinden hangi alanların dönüleceği belirtilir. Bu filtreyi kullanmadan önce Startup.cs içerisinde bu filtreyi kullanıma açtığımızı belirtiyoruz, aksi takdirde aşağıdaki hata mesajını alacağız.

The query specified in the URI is not valid. The property ‘…’ cannot be used in the $select query option.

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
	app.UseEndpoints(endpoints =>
	{
		endpoints.Select();
		// ...
	});
}

https://localhost:44357/odata/vehicles?$select=Id,Model adresine çağrı attığımız zaman yalnızca Vehicles sınıfından yalnızca Id ve Model alanlarının döndüğünü göreceğiz.

odata-select-query

$expand filtresi

Bu filtre yardımıyla EntitySet ve bu EntitySet’e bağlanmış navigation propertyleri de sorguyla birlikte getirmek için kullanılır. Yine bu filtreyi kullanıma açmamız gerekmektedir.

endpoints.Select().Expand();

https://localhost:44357/odata/vehicles?$expand=Manufacturer adresine çağrı attığımızda Vehicle sınıfının yanında Manufacturer bilgisinin de döndüğünü göreceksiniz.

odata-expand-query

$expand filtresiyle birlikte Manufacturer sınıfı içerisinde $select filtresini kullanmak için: https://localhost:44357/odata/vehicles?$expand=Manufacturer($select=Id,Name) adresine istek atmamız yeterli.

Category sınıfı üzerinden Vehicle sınıflarını ve bağlı olduğu Manufacturer bilgisini çekmek için ise: https://localhost:44357/odata/categories?$expand=Vehicles($select=Id,Model;$expand=Manufacturer) adresine istek atmamız yeterli. Burada parantez içerisine girdiğimiz zaman filtreleri “;” ile ayırdığımıza dikkat edin.

$orderby filtresi

Bu filtre yardımıyla isminden de anlaşılacağı üzere belirtilen bir propery üzerinde sıralama işlemi gerçekleştiriyoruz. Yine bu filtreyi kullanıma açmamız gerekmektedir.

endpoints.Select().Expand().OrderBy();

Vehicles sınıfından Id, Model ve CreatedOn alanlarını seçerek CreatedOn üzerinde azalan (descending) sıralama işlemi gerçekleştirelim.

https://localhost:44357/odata/vehicles?$select=Id,Model,CreatedOn&$orderby=CreatedOn desc adresine istek attığımız zaman aşağıdaki gibi bir veri seti elde etmeliyiz.

odata-orderby-filter

Ayrıca ikinci bir sıralamada bulunmak istiyorsak $orderby filtresini $orderby=CreatedOn desc,Model asc şeklinde belirtmemiz yeterli.

$top filtresi

Seçtiğimiz dizinden kaç eleman almak istediğimizi belirttiğimiz filtredir, yine bu özellik aktif edilmelidir. Bu özellik aktif edilirken en fazla kaç eleman seçilebileceğine dair boş geçilebilir bir parametre almaktadır, bu alanı bir değer vererek aktif etmekte fayda var, aşağıda en fazla 100 kayıt seçilebileceği belirtilmiştir.

endpoints.Select().Expand().OrderBy().MaxTop(100);

Vehicles sınıfından yine $select ve $orderby filtreleri gerçeklendikten sonra üstten ilk 3 kaydı seçmek için https://localhost:44357/odata/vehicles?$select=Id,Model,CreatedOn&$orderby=CreatedOn desc&$top=3 adresine istek atılması yeterlidir.

odata-top-filter

$skip filtresi

Seçtiğimiz dizinden kaç elemanın atlanacağını belirttiğimiz filtredir, $top filtresi ile birlikte kullanıldığında sayfalama (paging) özelliği kazandırılmış olur. Bu özellik için bir aktivasyon söz konusu değildir, varsayılan olarak gelmektedir.

https://localhost:44357/odata/vehicles?$select=Id,Model,CreatedOn&$orderby=CreatedOn desc&$top=3&$skip=3 adresine istek attığımızda tarihe göre azalan sırada sıralanmış ve üstten ilk 3 kaydın atlanarak sonraki 3 kaydın seçildiğini görürüz.

odata-skip-top-query

Sayfalamayı bir alternatif olarak [EnableQuery] içerisinden de yapabiliriz. Bu durumda mevcut kodumuzu [EnableQuery(PageSize = 2)] şeklinde değiştirmemiz yeterli. Bu durumda yalnızca $skip filtresini kullanmamız yeterli olacaktır, ayrıca dönen veri seti içerisine @odata.nextLink isminde bir düğüm eklendiğine de dikkat edelim, bir sonraki sayfalama verisi olduğu sürece sonraki sayfa bilgisi bu düğüm içerisinden elde edilebilir.

odata-enablequery-pagesize-skip

$count filtresi

İsminden de anlaşılacağı üzere filtreleme sonucu toplam kaç kaydın olduğu bilgisini verir. Yine bu özelliği aktif etmemiz gerekmektedir.

endpoints.Select().Expand().OrderBy().MaxTop(100).Count();

https://localhost:44357/odata/vehicles?$skip=2&$top=2&$orderby=Id desc&$count=true adresine istek attığımız zaman toplam kayıt sayısının @odata.count içerisinde olduğu görülecektir. Ayrıca kayıt bilgisi gelsin istemiyorsak $count=false ile bu işlemi gerçekleştirebiliriz.

odata-count-filter

Category verisi üzerinden Vehicles verilerini de çektiğimizi düşünelim, eğer Vehicles verisine ilişkin $count bilgisini istiyorsak bu işlemi çok basit bir şekilde gerçekleştirebiliriz.

https://localhost:44357/odata/categories?$expand=Vehicles($count=true)&$count=true adresine istek attığımızda Vehicles düğümü altında [email protected] şeklinde toplam kayıt sayısının dönüldüğü görülecektir.

odata-expand-count-filter

$filter filtresi

OData‘nın asıl güçlü olduğu ve bir çok özellik sunan son filtresine geldik. Burada üç tür filtrelemeyi ele alacağız; mantıksal, aritmetiksel ve fonksiyonlar. İsminden de anlaşılacağı üzere dizinleri filtrelemeye yarar, filtre yine aktif edilmelidir.

endpoints.Select().Expand().OrderBy().MaxTop(100).Count().Filter();

Mantıksal Operatörler

OData bize $filter filtresi ile birlikte aşağıdaki mantıksal operatörleri sunar.

  • eq (equal): $filter=Name eq ‘Bmw’
  • ne (not equal): $filter=Name ne ‘Bmw’
  • gt (greater than): $filter=Name gt ‘Mazda’
  • ge (greater than or equal): $filter=Name ge ‘Toyota’
  • lt (lower than): $filter=Name lt ‘Volvo’
  • le (lower than or equal): $filter=Name le ‘Volvo’
  • or: $filter=Name eq ‘Bmw’ or Name eq ‘Volvo’
  • and: $filter=Name eq ‘Bmw’ and Year lt 2010

Beygir gücü 100’den büyük Vehicle verilerini listelemek için https://localhost:44357/odata/vehicles?$filter=Engine gt 100 adresini kullanabiliriz ya da tarihi 1 Ocak 2020 tarihinden büyük olan kayıtları https://localhost:44357/odata/vehicles?$filter=CreatedOn ge 2020-01-01 adresiyle süzerek listeleyebiliriz.

Aritmetiksel Operatörler

OData bize $filter filtresi ile birlikte aşağıdaki aritmetik operatörleri sunar.

  • add (addition): $filter=Year add 2 eq 2021
  • sub (subtracts): $filter=Year sub 2 eq 2019
  • mul (multiples): $filter=Doors mul 2 eq 10
  • div (divides): $filter=Doors div 2 eq 2
  • mod (modulo): $filter=Doors mod 5 eq 0

Canonical Fonksiyonlar

OData bize $filter filtresi ile birlikte kullanabileceğimiz aşağıdaki canonical fonksiyonları sunar, kullanım şekilleri yanlarında verilmiştir.

  • substringof (string, string): $filter=substringof(‘Bmw’, Name) eq true
  • endswith (string, string): $filter=endswith(BodyType, ‘Coupe’)
  • startswith (string, string): $filter=startswith(Name, ‘Li’)
  • length (string): $filter=length(Name) eq 7
  • indexof (string, string): $filter=indexof(Name, ‘Toy’) eq 0
  • replace (string, string, string): $filter=replace(Model, ‘ ‘, ”) eq ‘1Series’
  • substring (string, int): $filter=substring(Name, 1) eq ‘azda’
  • tolower (string): $filter=tolower(Name) eq ‘volvo’
  • toupper (string): $filter=toupper(Name) eq ‘BMW’
  • trim (string): $filter=length(trim(Name)) eq length(Name)
  • concat (string, string): $filter=concat(concat(Generation, ‘, ‘), Model) eq ‘XC90 II, XC90’
  • year (datetime): $filter=year(CreatedOn) eq 2020
  • month (datetime): $filter=month(CreatedOn) eq 5
  • day (datetime): $filter=day(CreatedOn) eq 7
  • hour (datetime): $filter=hour(CreatedOn) eq 2
  • minute (datetime): $filter=minute(CreatedOn) eq 59
  • second (datetime): $filter=second(CreatedOn) eq 59
  • round (double): $filter=round(Weight) eq 2000
  • floor (double): $filter=floor(Weight) eq 1000
  • ceiling (double): $filter=ceiling(Freight) eq 2000
  • isof (type): $filter=isof(Api.Models.Vehicle)
  • cast (type)

Yukarıda bahsettiğimiz operatörleri parantez operatörüyle birbirlerine bağlayabileceğinizi veya gruplayabileceğinizi de unutmayın.

OData ile ilgili olarak diğer yazılarıma da göz atabilirsiniz.

You may also like...

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir