ASP.NET Core Web API ile JSON PATCH Kullanımı

ASP.NET Core Web API ile JSON PATCH Kullanımı

Bu hayatı, istesek de istemesek de dinamik bir biçimde yaşıyoruz. Hemen hiçbir şey aynı kalmıyor. Özellikle, yaşam alanımızdaki birçok varlığı zaman içinde güncelliyor, tamamen yeniliyoruz.

Örneğin, oturma odanızdaki koltuklarınız bir süre sonra işlevsiz bir hale geldiğinde (ya da canınız sıkıldığında) onları yenileme ihtiyacı duyuyorsunuz. Ya da kim bilir belki de evinize yeni bir oda eklemeyi bile planlıyor olabilirsiniz.

Yazılım uygulamalarında kullandığımız varlıklarda da durum aynı. Elbette, onları da  güncellemeye açık bir biçimde yönetebilmemiz gerekiyor. Fakat kendi hayatımızdaki bir varlığı topyekün yenilemenin masraflı olması gibi, yazılımsal bir varlığı da tamamen güncellemek maliyetli olabiliyor.

Bu maliyeti indirgemek için gerçek hayatta ne yapıyoruz? Komple bir yenileme yerine, sadece belli bir özelliği değiştiriyoruz. Örneğin eski koltuk takımını tamamen çıkarıp yerine yenisini almaktansa, sadece kumaşını değiştirme şansına sahibiz.

Peki, bir RESTFul servis mimarisinde böyle bir yapı uygulayabilir miyiz?

Normal şartlarda bir Web API uygulamasında, sunucudaki bir varlığın güncellenmesi için istemciden o varlığın tüm özelliklerini isteriz. Sonra da söz konusu güncellemeyi yapar ve istemciye “200” yanıtını döndürürüz.

Projeye Başlıyoruz!

Elbette bu noktada kod tarafına geçip, biraz daha elle tutulur bir örnek üzerinden yürümek daha şık olacak. Bir .NET Core 3.1 Web API projesi altında şu modelleri oluşturarak işe başlayalım.

public class Player
{
    public int Id { get; set; }
    public string Name { get; set; }
    public DateTime BirthDate { get; set; }
    public string Position { get; set; }
    public string Nationality { get; set; }

}    
public class Team
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string LogoUrl { get; set; }
    public List<Player> Players { get; set; }

}

Elbette bu iki modelim de bana bir veri sunmak için tasarlandı. Ancak bu veriyi doğrudan veritabanından almayacağım. Bunun yerine, bellekte hazır bir veriyi tutacağım. Ne de olsa amacım, RESTFul bir serviste PATCH işlemini anlatmak. Dolayısıyla işin özünde kalmak adına veritabanı işlemlerine girmiyorum hiç.

Tabii yine de kodu temiz tutmak amacıyla, aşağıdaki Fake koleksiyonu tanımladım:

public class InMemoryTeamsAndPlayers
    {
        public static List<Team> GetTeamsInMemory()
        {
            return new List<Team>
            {
                 new Team
                 {
                      Id =1,
                      Name = "Beşiktaş JK",
                      Players = new List<Player>
                      {
                          new Player{
                                     Name ="Utku Yuvakuran",
                                     BirthDate = new DateTime(1997,11,2),
                                     Nationality="Türkiye",
                                     Position ="Kaleci"
                                    },
                          new Player { 
                                       Name="Ersin Destanoğlu",
                                       BirthDate = new DateTime(2001,1,1),
                                       Nationality ="Türkiye",
                                       Position = "Kaleci"
                                     },
                          new Player { 
                                       Name="Domagoj Vida",
                                       BirthDate = new DateTime(1989,4,29),
                                       Nationality ="Türkiye",
                                       Position = "Stoper"
                                     }
                      }

                 },
                     new Team
                 {
                      Id=2,
                      Name = "Galatasaray SK",
                      Players = new List<Player>
                      {
                          new Player{
                                     Name ="Fernando Muslera",
                                     BirthDate = new DateTime(1986,6,16),
                                     Nationality="Türkiye",
                                     Position ="Kaleci"
                                    },
                          new Player { 
                                       Name="Okan Kocuk",
                                       BirthDate = new DateTime(1995,7,27),
                                       Nationality ="Türkiye",
                                       Position = "Stoper"
                                     },
                           new Player { 
                                       Name="Christian Luyindama",
                                       BirthDate = new DateTime(1994,1,8),
                                       Nationality ="Kongo",
                                       Position = "Kaleci"
                                     }
                      }

                 }

            };
        }
    }

Elbette, bu In-Memory koleksiyon üzerinde işlem yapmak için bir de Service sınıfına ihtiyacım olacak.

public class TeamService
    {
        public List<Team> GetTeams()
        {
            return InMemoryTeamsAndPlayers.GetTeamsInMemory();
        }

        public Team GetTeam(int id)
        {
            return InMemoryTeamsAndPlayers.GetTeamsInMemory().FirstOrDefault(x => x.Id == id);
        }

        public Team Edit(Team team)
        {
            var existingTeam = GetTeam(team.Id);
            if (existingTeam != null)
            {
                existingTeam.LogoUrl = team.LogoUrl;
                existingTeam.Name = team.Name;
                existingTeam.Players = team.Players;
                
            }

            return existingTeam;

        }
    }

Dediğim gibi, konuyu bağlamından uzaklaştırmamak adına servis içine insert operasyonunu eklemedim. Burada önceliğimiz sadece, güncelleme işlemine odaklanmak.

Pekala, böyle bir durumda API istemcisinin güncelleme yapmasını istiyorsak, nasıl bir metot yazacağız? Akla gelen en geleneksel metot, elbette http PUT metodu olacaktır.

O halde, TeamsController isimli API Controller sınıfımı aşağıdaki biçimde oluşturuyorum:

public class TeamsController : ControllerBase
    {
        public IActionResult Get()
        {
            var teams = new TeamService().GetTeams();
            return Ok(teams);
        }

        [HttpPut("{id}")]
        public IActionResult Edit(int id, Team team)
        {
            TeamService teamService = new TeamService();
            if (teamService.GetTeam(id) == null)
            {
                return BadRequest();
            }
            Team updatedTeam = teamService.Edit(team);
            return Ok(updatedTeam);
        }
    }

PUT’un neyi eksik canım?

Peki. İşte kritik bir noktadayız. Burada yer alan http PUT metodunun Team nesnesinin bir örneğini (instance) parametre olarak aldığına dikkat edelim. Bu ne demek? İstemci, bu eylemi çalıştırmak istiyorsa; Team varlığının TAMAMINI request içerisinde göndermek zorunda. Yani bu durumda, request içerisinde yer alan JSON verisi aşağıdaki gibi olmalı:

(url: https://localhost:44315/api/teams/1)

{
    "id": 1,
    "name": "Beşiktaş JK",
    "logoUrl": null,
    "players": [       
        {
            "id": 0,
            "name": "Ersin Destanoğlu",
            "birthDate": "2001-01-01T00:00:00",
            "position": "Kaleci",
            "nationality": "Türkiye"
        },
        {
            "id": 0,
            "name": "Domagoj Vida",
            "birthDate": "1989-04-29T00:00:00",
            "position": "Stoper",
            "nationality": "Türkiye"
        }
    ]
}

İyi ama, ben sadece Domagoj Vida’nın ülkesini güncellemek istiyorsam? Yani tek istediğim Team varlığının Players koleksiyonu içinde yer alan ikinci nesnenin “nationality” özelliğini Hırvatistan olarak değiştirmekse? Ama bunun için bütün Team varlığını oluşturup sunucuya göndermem gerekiyor!

Yani koltuğun sadece kılıfını değiştirebilecekken, tamamını yeniden sipariş vermem gibi bir şey bu!

İşte PATCH burada imdadımıza yetişiyor.

HttpPatch operasyonu

Şekildeki gibi bir JSON kaynağının belirli bir kısmını değiştirmek (ya da eklemek) istiyorsam, HttpPatch operasyonunu kullanıyoruz.

Dilerseniz ilk olarak, bu amaçla yazılmış bir PATCH Http request’in JSON içeriğine bakalım:

[
    {
        "op":"replace",
        "path":"/players/2/nationality",
        "value":"Hırvatistan"
    }
]

Şu kibarlığa bir bakın hele! Sadece ne iş yapacağını (“op”), nereye müdahale edeceğini, (“path”) ve veriyi (tabii ki “value”) söylememiz yeterli. Daha düşük karmaşıklık, hayat kurtarır.

Buradan anlayacağınız gibi PATCH sadece bir veriyi bir başkasıyla değiştirmek (replace) için kullanılmıyor. Başka operasyonları da var. Tamamı şöyle; “add”, “remove”, “replace”, “move”, “copy” ve “test”.

Peki, bu request türünü ASP.NET Core Web API içerisinde nasıl kullanacağız?

İlk olarak projemize,

 Microsoft.AspNetCore.Mvc.NewtonsoftJson

Nuget paketini eklememiz gerekiyor. Sonra da projemizin Startup sınıfında bulunan ConfigureServices metodunda, AddNewtonsoftJson() extension metodunu çağırıyoruz:

public void ConfigureServices(IServiceCollection services)
{
      services.AddControllers()
              .AddNewtonsoftJson();
}

DİKKAT!

Bu metodu çağırdığınızda, projenizde kullandığınız tüm JSON içeriğinin formatını ezmiş olursunuz (normalde System.Text.Json kütüphanesi kullanılır).

Artık JSON Patch için gerekli Action metodumuzu oluşturabiliriz.

http PATCH eylemi için Action metodu

[HttpPatch("{id}")]
public IActionResult Patch(int id,JsonPatchDocument<Team> patchDocument)
{
    if (patchDocument == null)
    {
       return BadRequest(ModelState);
    }

    var team = new TeamService().GetTeam(id);
    if (team == null)
    {
         return NotFound();
    }

    patchDocument.ApplyTo(team,ModelState);

    if (ModelState.IsValid)
    {
        return Ok(team);
    }

    return BadRequest(ModelState);
} 

Yukarıda gördüğünüz action metodunun olmazsa olmazlarına bakalım dilerseniz.

  1. [HttpPatch(“{id}”)] Attribute’ü ile işaretlenmiş durumda (eh bu normal tabii)
  2. Metod, JsonPatchDocument<Team>   nesnesi alıyor! İşte bu önemli. Yukarıda örneğini gördüğümüz bir JSON Patch request’i içerisindeki komutları yorumlayan taşıyıcı nesnemiz, JsonPatchDocument<> nesnesi. Tahmin edebileceğiniz gibi request ile gelen talebin hangi varlığa (bizim örneğimizde Team) uygulanması gerektiğini jenerik olarak belirtiyoruz.
  3. patchDocument.ApplyTo(team,ModelState);  metoduyla değişikliği uygulamasını söylüyoruz. Buradaki ModelState parametresi üzerinde biraz duralım. Ya PATCH içindeki komutun uygulanması sırasında bir hata oluşursa? Böyle bir durumda ASP.NET MVC’nin alametifarikalarından biri olan ModelState nesnesine hatayı yazdırmak iyi olurdu değil mi?  İşte o ikinci parametre tam olarak bunu yapıyor.

O zaman Postman aracılığıyla talebimizi gönderelim bakalım.

Giden Talep:

[
    {
        "op":"replace",
        "path":"/players/2/nationality",
        "value":"Hırvatistan"
    }
]

Ve dönen yanıt:

{
    "id": 1,
    "name": "Beşiktaş JK",
    "logoUrl": null,
    "players": [
        {
            "id": 0,
            "name": "Utku Yuvakuran",
            "birthDate": "1997-11-02T00:00:00",
            "position": "Kaleci",
            "nationality": "Türkiye"
        },
        {
            "id": 0,
            "name": "Ersin Destanoğlu",
            "birthDate": "2001-01-01T00:00:00",
            "position": "Kaleci",
            "nationality": "Türkiye"
        },
        {
            "id": 0,
            "name": "Domagoj Vida",
            "birthDate": "1989-04-29T00:00:00",
            "position": "Stoper",
            "nationality": "Hırvatistan"
        }
    ]
}

Önceden de belirttiğim gibi, JSON PATCH ile başka operasyonlar da yapılabilir. Örneğin gelin yeni bir futbolcu ekleyelim:

[
    {
        "op":"add",
        "path":"/players/-",
        "value":{
           "id": 3,
            "name": "Erdoğan Kaya",
            "birthDate": "2001-03-27T00:00:00",
            "position": "Stoper",
            "nationality": "Türkiye"
        }
    }
]

Ya da JSON kaynağı içerisinde, silme, kopyalama ve taşıma işlemleri yapabilirsiniz. Bunları size bırakıyorum.

Fakat benim en başarılı bulduğum operasyon “test” operasyonu. Diyelim ki, JSON kaynağımızda koşullu bir değişiklik yapmak istiyoruz. Örneğin “takımın ilk oyuncusunun adı Utku Yuvakuran ise pozisyonunu stoper olarak değiştir” demek istiyoruz. İşte o zaman şöyle yazıyoruz:

[
    {
        "op":"test",
        "path":"/players/0/name",
        "value":"Utku Yuvakuran"
    },
    {
        "op":"replace",
        "path":"/players/0/position",
        "value":"Stoper"
    }
]

Eğer kaynaktaki adreste (“players/0/name”) bulunan değer Utku Yuvakuran değilse, değiştirme işlemini yapmayacaktır, çünkü testten geçememiş olacak. Böylece, hatalı bir değişiklik yapma ihtimalinizi en aza indirmiş oluyorsunuz!

Evet sevgili dostlar! Böylece, bir yazımızın daha sonuna gelmiş olduk. Elbette konu hakkındaki yorumlarınızı bekliyorum. Tabii bu arada eğer bu içeriği beğendiyseniz beni takip listenize ekleyebilirsiniz.

Hah neredeyse unutuyordum. Projenin kaynak kodları elbette burada:

https://github.com/turkayurkmez/PatchOperations/

Kendinize iyi bakın!

2 thoughts on “ASP.NET Core Web API ile JSON PATCH Kullanımı

  1. Elinize sağlık, fake data içinde Fenerbahçe’nin yer almaması sevindirdi zira diğer takımlar gibi fake bir takım değildir.

    1. Baştan söyleyeyim, Beşiktaş’lı değilim (takım tutmuyorum). Direkt rastgele aldığım bir takım bilgisiydi 🙂

Leave a Reply