İçeriğe atla

ASP.NET Core 6/Web Servisleri/İçerik Biçimlendirme

Vikikitap, özgür kütüphane

Şimdiye kadar action metotlarımız sadece JSON döndürdü. Ancak bu tek seçenek değildir. Action metotlarımız JSON, XML ve ham metin döndürebilir. Bir action metodun belirli bir biçimde veri döndürmesi demek basitçe Content-Type header'ını ilgili biçime göre ayarlaması ve asıl veriyi de gövdeye yazması demektir. Elbette Content-Type header'ının ayarlanması ve response'un gövdesine bir şeyler yazılması arkaplanda ASP.NET Core tarafından yapılır. Biz sadece yaptığımız ayarlarla action metotların belirli bir biçimde veri döndürmesini sağlarız.

Bir action metodun döndürdüğü veri biçiminin belirlenmesi 4 parametreye bağlıdır:

  1. İstemcinin kabul ettiği biçimler
  2. Sunucunun üretebildiği biçimler
  3. Action metodun tabi olduğu içerik politikası
  4. Action'ın döndürdüğü veri tipi

Varsayılan içerik politikası

[değiştir]

Yukarıda saydığımız 4 faktörün hep beraber biçime nasıl etki ettiğini irdelemek kafa karıştırıcı olabilir. Şimdilik en basit senaryoyla ilerleyelim. İstemci her türden veri kabul etsin, sunucu her türden veri gönderebilsin ve varsayılan içerik politikası kullanılsın. Bu durumda belirlenecek biçime tek karar veren faktör metodun geri dönüş tipi olacaktır. Böyle bir durumda eğer metodun geri dönüş tipi string ise bu string olduğu gibi istemciye gönderilir. Content-Type header'ı "text/plain" olur. Eğer metodun geri dönüş tipi string değilse belirlenen biçim JSON olur. Bu durumdaki tipler karmaşık tipler veya int, double, vb. gibi temel veri tipleri olabilir.

String tipi diğer tiplere göre özel bir muamele görmektedir. ASP.NET Core diğer temel veri tiplerinin değerlerini çift tırnak içine alarak JSON formatında istemciye göndermektedir. Eğer aynısı string tipinde olacak olsaydı ""metin"" gibi tuhaf bir sentaks oluşurdu. Her istemci bu tür bir JSON değeriyle nasıl başa çıkılacağını bilemeyebilir. O yüzden olası sorunları en başından elimine etmek için ASP.NET Core stringleri direkt tırnak eklemeden text/plain olarak göndermektedir.

Şimdi projemizin Controllers klasörüne ContentController.cs isimli bir controller ekleyelim ve içeriği şöyle olsun:

using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using WebApp.Models;
namespace WebApp.Controllers
{
    [ApiController]
    [Route("/api/[controller]")]
    public class ContentController : ControllerBase
    {
        private DataContext context;
        public ContentController(DataContext dataContext)
        {
            context = dataContext;
        }
        [HttpGet("string")]
        public string GetString() => "This is a string response";
        [HttpGet("object")]
        public async Task<Product> GetObject()
        {
            return await context.Products.FirstAsync();
        }
    }
}

Bu örnekteki GetString() action'ına talepte bulunursanız geriye "text/plain" biçimde "This is a string response" cevabı alırısnız. Eğer GetObject() action'ına talepte bulunursanız JSON tipindeki Product nesnesine ulaşırsınız.

İçerik biçimi uzlaşması

[değiştir]

Çoğu durumda istemci ile sunucu içeriğin hangi biçimde sunulacağı konusunda bir uzlaşmaya varırlar. İstemci belirli bir biçimde veri ister, sunucu o biçimde veri veremeyeceğini söyler, istemci "ondan veremiyorsan bari şundan ver" der. En sonunda ortak bir karar alırlar ve veri o biçimde gönderilir. Bir nevi istemci ile sunucu pazarlık yapar. Bu bölümde uygulamanın içerik politikası ve action metodun geri dönüş tipi sabit kalacak, istemcinin kabul ettiği veri biçimleri değişecektir.

İstemci sunucuya bir talep gönderdiğinde istemcinin hangi biçimlerde veri kabul ettiği Accept header'ında belirli bir formatta belirtilir. Örnek bir Accept header'ı aşağıdaki gibidir:

Accept:
text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9

Accept header'ında belirtilen kabul edilen biçimler virgül ile ayrılmaktadır. Bu örneğimizde istemci HTML, XHTML, XML veri biçimlerini; avif, webp, apng resimlerini ve signed-exchange biçimindeki veriyi kabul etmektedir.

Bir içerik biçiminden noktalı virgül ile ayrılan q değeri o içeriğin biçiminin önceliğini belirtmektedir. q için 1 değeri maksimum öncelik demektir ve hiç q parametresi kullanılmayan duruma denktir. 1'den aşağı doğru inildikçe önceliğin azalacağı anlamına gelir. Yani istemci 1'den küçük q değerli içerik biçimleri için "bu içerik biçimini kabul ediyorum, ancak çok tercih etmiyorum, mümkünse önceliği daha yüksek olan bir tipte gönder" demektedir. Bu örneğimizde header XML'i kabul etmekte ama mümkünse HTML ve XHTML'i tercih etmektedir.

*/* değeri istemcinin herhangi bir tipte veri kabul edebileceğini belirtir. Ancak örneğimizde bunun önceliği düşüktür (0.8). Bir fallback (son çare) içerik tipi belirtimidir.

Bütün bunları birleştirdiğimizde istemcinin kabul ettiği tipler öncelik sırasına göre aşağıda verilmiştir:

  1. HTML, XHTML, avif, webp, apng (puan: 1)
  2. XML, signed-exchange (puan: 0.9)
  3. Diğer biçimler (puan:0.8)

Şimdi bu aşamada istemcinin gönderdiği Accept header'ında değişiklik yaparak istemcinin istediği biçimde veri alabileceğini düşünebilirsiniz. İstemcinin Accept header'ında belirttiği değerler alacağı biçimin belirlenmesinde etkilidir, ancak istemcinin istediği biçimde veri almasını garantilemez. Örneğin bir request sadece XML alabileceğini ASP.NET Core sunucusuna iletse şu aşamada sunucunun yine de string dışındaki tipler için JSON döndürdüğünü görürüz.

Varsayılan durumda ASP.NET Core string dışındaki tipler için yalnızca JSON döndürür. ASP.NET Core'un XML de döndürebilmesi için uygulamamızda birtakım yapılandırmalar yapmamız gerekir.

XML biçimlendirmeyi etkileştirme

[değiştir]

İçerik biçimi uzlaşmasının çalışması ve XML biçiminde içerik üretebilmek için Program.cs dosyanızı şöyle değiştirin:

using Microsoft.EntityFrameworkCore;
using WebApp.Models;
using Microsoft.AspNetCore.Mvc;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<DataContext>(opts => {
    opts.UseSqlServer(builder.Configuration["ConnectionStrings:ProductConnection"]);
    opts.EnableSensitiveDataLogging(true);
});
builder.Services.AddControllers().AddNewtonsoftJson().AddXmlDataContractSerializerFormatters();
builder.Services.Configure<MvcNewtonsoftJsonOptions>(opts => {
    opts.SerializerSettings.NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore;
});
var app = builder.Build();
app.MapControllers();
app.MapGet("/", () => "Hello World!");
var context = app.Services.CreateScope().ServiceProvider.GetRequiredService<DataContext>();
SeedData.SeedDatabase(context);
app.Run();

Bu kodumuzla uygulamanın kullandığı servislere AddXmlDataContractSerializerFormatters() servisi eklenmiştir. Şimdi ContentController.cs dosyamızı şöyle değiştirelim:

using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using WebApp.Models;
namespace WebApp.Controllers
{
    [ApiController]
    [Route("/api/[controller]")]
    public class ContentController : ControllerBase
    {
        private DataContext context;
        public ContentController(DataContext dataContext)
        {
            context = dataContext;
        }
        [HttpGet("string")]
        public string GetString() => "This is a string response";
        [HttpGet("object")]
        public async Task<ProductBindingTarget> GetObject()
        {
            Product p = await context.Products.FirstAsync();
            return new ProductBindingTarget()
            {
                Name = p.Name,
                Price = p.Price,
                CategoryId = p.CategoryId,
                SupplierId = p.SupplierId
            };
        }
    }
}

Artık programımızdaki GetObject() action'ı eğer request sadece XML alabileceğini belirtmişse geriye XML veri döndürecektir. Diğer bir deyişle istemcinin seçimine saygı duymaktadır.

İstemcinin tercihlerine tam anlamıyla saygı duymak

[değiştir]

Artık uygulamamız istemcinin tercihlerine saygı duyuyor ama tam anlamıyla da saygı duymuyor. İstemci sadece XML alabileceğini belirttiğinde XML gönderiyor. Ama örneğin istemci düşük öncelikli de olsa */* tipine izin vermişse uygulamamız sonucu JSON ile gönderecektir. İstemci isterse JSON'a 0.1, XML'e 1 puan versin, gelen seçenekler arasında doğrudan veya dolaylı olarak JSON varsa sonucu JSON olarak göndermektedir. Bu saygı eksikliğinin bir başka sonucu da eğer istemci gönderilecek JSON verisi için uygun olmayan bir içerik tipi belirttiyse sunucunun geriye hata kodu döndürmek yerine yine JSON göndermesidir. Örneğin istemci Accept header'ında sadece "img/png" tipine izin vermişse ve JSON (veya XML)'e uygun veri döndüren bir action'a talepte bulunuyorsa geriye JSON dönecektir. Sunucunun istemcinin içerik biçimi tercihlerine tam anlamıyla saygı duyması için Program.cs dosyamızı şöyle değiştiririz:

using Microsoft.EntityFrameworkCore;
using WebApp.Models;
using Microsoft.AspNetCore.Mvc;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<DataContext>(opts => {
    opts.UseSqlServer(builder.Configuration["ConnectionStrings:ProductConnection"]);
    opts.EnableSensitiveDataLogging(true);
});
builder.Services.AddControllers().AddNewtonsoftJson().AddXmlDataContractSerializerFormatters();
builder.Services.Configure<MvcNewtonsoftJsonOptions>(opts => {
    opts.SerializerSettings.NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore;
});
builder.Services.Configure<MvcOptions>(opts => {
    opts.RespectBrowserAcceptHeader = true;
    opts.ReturnHttpNotAcceptable = true;
});
var app = builder.Build();
app.MapControllers();
app.MapGet("/", () => "Hello World!");
var context = app.Services.CreateScope().ServiceProvider.GetRequiredService<DataContext>();
SeedData.SeedDatabase(context);
app.Run();

Bu kodumuza MvcOptions ayar sınıfı ile uygulamamız yapılandırılmaktadır. Bu sınıfın RespectBrowserAcceptHeader özelliği ile sunucunun istemcinin Accep header'ındaki tercihlere tam olarak saygı duyması sağlanmaktadır. ReturnHttpNotAcceptable özelliği ile de eğer istemci uygun olmayan bir tipte talepte bulunuyorsa geriye HTTP 406 kodu döndürülecektir.

Bir action'ın içerik biçimini belirtme

[değiştir]

Şimdiye kadar bir metodun geri dönüş tipiyle veya istemcinin gönderdiği Accept header'ındaki değerler ile içerik biçimlendirmesi belirlendi. Şimdi ASP.NET Core tarafında elle içerik biçimlendirmesini belirleyeceğiz. ASP.NET Core tarafında içerik biçimi belirlendiği zaman Accept header'ındaki değerler geçersiz olur. Örnek (ContentController.cs dosyası):

using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using WebApp.Models;
namespace WebApp.Controllers
{
    [ApiController]
    [Route("/api/[controller]")]
    public class ContentController : ControllerBase
    {
        private DataContext context;
        public ContentController(DataContext dataContext)
        {
            context = dataContext;
        }
        [HttpGet("string")]
        public string GetString() => "This is a string response";
        [HttpGet("object")]
        [Produces("application/json")]
        public async Task<ProductBindingTarget> GetObject()
        {
            Product p = await context.Products.FirstAsync();
            return new ProductBindingTarget()
            {
                Name = p.Name,
                Price = p.Price,
                CategoryId = p.CategoryId,
                SupplierId = p.SupplierId
            };
        }
    }
}

Burada Produces attribute'u ile GetObject() action'ının JSON üreteceğini belirttik. Produces attribute'u için birden fazla değer belirtebiliriz. Bu durumda istemcinin Accept header'ındaki tercihler geçerli olacaktır.

URL ile kabul edilen biçimin belirtilmesi

[değiştir]

Varsayılan kabul edilen biçimin belirtilmesi işlemi Accept header'ı ile olur. Ama URL üzerinden de bu tercih yapılabilir. Örnek:

using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using WebApp.Models;
namespace WebApp.Controllers
{
    [ApiController]
    [Route("/api/[controller]")]
    public class ContentController : ControllerBase
    {
        private DataContext context;
        public ContentController(DataContext dataContext)
        {
            context = dataContext;
        }
        [HttpGet("string")]
        public string GetString() => "This is a string response";
        [HttpGet("object/{format?}")]
        [FormatFilter]
        [Produces("application/json", "application/xml")]
        public async Task<ProductBindingTarget> GetObject()
        {
            Product p = await context.Products.FirstAsync();
            return new ProductBindingTarget()
            {
                Name = p.Name,
                Price = p.Price,
                CategoryId = p.CategoryId,
                SupplierId = p.SupplierId
            };
        }
    }
}

Bu örneğimizde GetObject() action'ı 3 tane attribute almaktadır. HttpGet attribute'u "object/<format>" path'ına yapılan GET request'lerini GetObject() action'ına yönlendirmektedir. Buradaki format parametresi opsiyoneldir. Produces attribute'u ilgili action'ın JSON veya XML üretebileceğini belirtmektedir. FormatFilter attribute'u ise hangi biçimlendirmenin seçileceğini request'in path'ındaki format isimli segment değişkeninin değerine göre yapmaktadır. Eğer request'in path'ı "api/content/object/xml" olursa GetObject() action'ı geriye XML döndürür. Eğer request'in path'ı "api/content/object/json" olursa GetObject() action'ı geriye JSON döndürür. Eğer request'in path'ı api/content/object" olursa geriye döndürülecek biçim Accept header'ındaki değerlere göre belirlenir.

Bir action'ın kabul ettiği biçimleri kısıtlama

[değiştir]

Şimdiye kadar hep bir action'ın geri döndürdüğü verinin biçimini belirtmeye çalıştık. Şimdi action'ın kabul ettiği veriyi belirteceğiz. Esasında bir action'ın kabul ettiği veri biçimlerinin geniş olmasında herhangi bir sıkıntı yoktur. Program.cs dosyasına eklenen servisler sayesinde action'lar hangi tipte veri üretebiliyorsa aynı tipte verileri deserialize etme yeteneğini de kazanmış olur. Aslında bir action'ın kabul ettiği veri biçimlerini kısıtlama ihtiyacı pek hissetmeyiz. Çünkü bütün süreç arkaplanda bizim yerimize yapılır. Eğer XML geldiyse XML deserializasyonu, JSON geldiyse JSON deserializasyonu yapılır. Ancak yine de ASP.NET Core bize bir metodun kabul ettiği içerik biçimini belirtme imkanı sağlamaktadır. Örnek (ContentController.cs):

using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using WebApp.Models;
namespace WebApp.Controllers
{
    [ApiController]
    [Route("/api/[controller]")]
    public class ContentController : ControllerBase
    {
        private DataContext context;
        public ContentController(DataContext dataContext)
        {
            context = dataContext;
        }
        [HttpGet("string")]
        public string GetString() => "This is a string response";
        [HttpGet("object/{format?}")]
        [FormatFilter]
        [Produces("application/json", "application/xml")]
        public async Task<ProductBindingTarget> GetObject()
        {
            Product p = await context.Products.FirstAsync();
            return new ProductBindingTarget()
            {
                Name = p.Name,
                Price = p.Price,
                CategoryId = p.CategoryId,
                SupplierId = p.SupplierId
            };
        }
        [HttpPost]
        [Consumes("application/json")]
        public string SaveProductJson(ProductBindingTarget product)
        {
            return $"JSON: {product.Name}";
        }
        [HttpPost]
        [Consumes("application/xml")]
        public string SaveProductXml(ProductBindingTarget product)
        {
            return $"XML: {product.Name}";
        }
    }
}

Bu sınıfa eklenen iki action sırasıyla yalnızca JSON ve yalnızca XML'e cevap vermektedir. İkisi de "api/content" path'ına yapılan POST request'lerine cevap vermektedir. Request'i karşılayacak action request'in Content-Type header'ına göre belirlenecektir. Eğer "api/content" path'ına Content-Type header'ı "application/xml" veya "application/json" olmayan bir POST request'i gelirse geriye HTTP 415 kodu döndürülür.