ASP.NET Core 6/Web Servisleri/Web Servislerini Geliştirme

Bu derste bir önceki derste yaptığımız controller tabanlı web servisini geliştireceğiz.

CORS'u etkinleştirme

değiştir

varsayılan durumda bir web sayfasını ilk olarak kim sunduysa JavaScript üzerinden sadece aynı sunucuya istek gönderilebilir. Örneğin başka bir sunucudan sunulan bir web sayfasının içindeki javaScript kodu varsayılan durumda bizim servisi kullanamaz. Bu güvenlik amacıyla alınmış bir önlemdir. Ancak bazen bu önlemi gevşetmek faydalı olabilir. CORS politikasını servis aboneliği ve middleware kaydı yaparak gevşetebiliriz. Şimdi Program.cs dosyanızı şöyle değiştirin:

using Microsoft.EntityFrameworkCore;
using WebApp.Models;
var MyAllowSpecificOrigins = "_myAllowSpecificOrigins";
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddCors(options =>
{
    options.AddPolicy(name: MyAllowSpecificOrigins, policy => policy.WithOrigins("http://example.com", "http://www.contoso.com"));
});

builder.Services.AddDbContext<DataContext>(opts => {
    opts.UseSqlServer(builder.Configuration["ConnectionStrings:ProductConnection"]);
    opts.EnableSensitiveDataLogging(true);
});
builder.Services.AddControllers();
var app = builder.Build();
app.UseCors(MyAllowSpecificOrigins);
app.MapControllers();
app.MapGet("/", () => "Hello World!");
var context = app.Services.CreateScope().ServiceProvider.GetRequiredService<DataContext>();
SeedData.SeedDatabase(context);
app.Run();

Bu örneğimizde artık http://example.com ve http://www.contoso.com web sitelerinin sayfaları bizim servisimize JavaScript üzerinden talep gönderebilecek.

Asenkron action'lar kullanma

değiştir

Action'ları asenkron kullanabiliriz. Örnek (ProductsController.cs):

using Microsoft.AspNetCore.Mvc;
using WebApp.Models;
namespace WebApp.Controllers
{
    [Route("api/[controller]")]
    public class ProductsController : ControllerBase
    {
        private DataContext context;
        public ProductsController(DataContext ctx)
        {
            context = ctx;
        }
        [HttpGet]
        public IAsyncEnumerable<Product> GetProducts()
        {
            return context.Products.AsAsyncEnumerable();
        }
        [HttpGet("{id}")]
        public async Task<Product> GetProduct(long id)
        {
            return await context.Products.FindAsync(id);
        }
        [HttpPost]
        public async Task SaveProduct([FromBody] Product product)
        {
            await context.Products.AddAsync(product);
            await context.SaveChangesAsync();
        }
        [HttpPut]
        public async Task UpdateProduct([FromBody] Product product)
        {
            context.Update(product);
            await context.SaveChangesAsync();
        }
        [HttpDelete("{id}")]
        public async Task DeleteProduct(long id)
        {
            context.Products.Remove(new Product() { ProductId = id });
            await context.SaveChangesAsync();
        }
    }
}

Asenkron action kullanmanın faydası daha az thread kullanarak çok daha fazla sayıda isteği eş zamanlı karşılayabilmektir. Action'ların senkron versiyonlarında bir thread action'ın başından sonuna kadar action'ın yürütülmesinden sorumludur. Her action'a bir thread atanır ve action'ın yürütülmesi esnasında thread başka amaçla kullanılamaz. Halbuki action'lar veritabanı erişimi gibi potansiyel olarak vakit alıcı ve işlemciye yüklenmeyen işlerle ilgilenebilirler, bu tür bir durumda işlemci boşta kalır ve thread başka bir amaçla kullanılabilir hale gelir. Senkron action'lar esasında zamanlarının büyük kısmını çevresel bileşenin işini yapmasını beklemekle geçirirler. Böyle bir durumda thread'in veritabanını beklemektense yeni request karşılamakla ilgilenmesi daha mantıklıdır. Veritabanı veriyi hazırlayıp action'a verdiğinde thread eski görevine geri döner.

ASP.NET Core'da veritabanıyla ilgili işlem yapan her metodun asenkron versiyonu yoktur. Örneğin kodmuzda Remove() ve Update() metotları senkron kullanılmak zorunda kalınmıştır.

Over-binding'in engellenmesi

değiştir

Bazen istemciler sunucuya gerekenden daha fazla veri gönderebilirler, bu durum çalışma zamanı hatası oluşmasına neden olabilir. Bu durumun önüne geçmeliyiz. Örneğin yukarıdaki SaveProduct() action'ında istemcinin Product için ProductId değeri göndermemesi gerekir. Çünkü veritabanına yeni kayıt eklenmesi işleminde ProductId değeri otomatik atanmaktadır. Eğer kullanıcı ProductId değeri gönderirse çalışma zamanı hatası oluşur. Over-binding'den sakınmanın bir yolu sadece bind edilecek özellikleri tutan yeni bir sınıf oluşturmaktır. Projenizin Models klasörüne ismi ProductBindingTarget.cs olan ve içeriği aşağıdaki gibi olan dosyayı ekleyin:

namespace WebApp.Models
{
    public class ProductBindingTarget
    {
        public string Name { get; set; }
        public decimal Price { get; set; }
        public long CategoryId { get; set; }
        public long SupplierId { get; set; }
        public Product ToProduct()
        {
            return new Product() { Name = this.Name, Price = this.Price, CategoryId = this.CategoryId, SupplierId = this.SupplierId };
        }
    }
}

Bu sınıf bind'lanacak özellikler yanında Product sınıfına dönüşümü yapacak metodu da içermektedir. Şimdi controller sınıfındaki SaveProduct() metodunu şöyle değiştirelim:

[HttpPost]
public async Task SaveProduct([FromBody] ProductBindingTarget target)
{
    await context.Products.AddAsync(target.ToProduct());
    await context.SaveChangesAsync();
}

Artık SaveProduct() metodu parametre olarak ProductBindingTarget kabul etmektedir. Gelen JSON verisinde ProductId için bir girdi olsa bile ProductBindingTarget sınıfında bu isimde bir özellik olmadığı için gelen ProductId verisi atılacaktır. Daha sonra ASP.NET Core tarafından oluşturulan ProductBindingTarget nesnesi Product nesnesine dönüştürülerek veritabanına yazılacaktır. Veritabanına gönderilirken ilgili Product'ın ProductId özelliğinin değeri 0 olacaktır, bu sayede veritabanına yazılırken herhangi bir sorun çıkmayacaktır.

Action sonuçları

değiştir

Bir action metodun işi bittiğinde ASP.NET Core olası gönderilecek değerlerle beraber uygun gördüğü bir HTTP durum kodunu gönderir. ASP.NET Core'un gönderdiği durum kodları çoğunlukla mantıklıdır, ancak bazen istediğiniz durum kodunu alamayabilirsiniz. Örneğin GetProduct() action'ından veritabanında olmayan id'li bir kayıt istediğinizde ASP.NET Core geriye HTTP 204 kodunu gönderir. Bu, başarılı bir request yapıldığını ancak request'e karşılık bir response döndürülmediğini belirtir. Böyle bir durumda HTTP 404 kodunu döndürmek daha mantıklı olabilir. Benzer şekilde normalde değer döndürmeyen action'ların değer döndürmesini isteyebiliriz. Örneğin SaveProduct() action'ının veritabanına yeni yazılan Product'ın id'siyle geri dönmesini isteyebiliriz.

Bir action metodu IActionResult arayüzünü uygulayan bir sınıf nesnesiyle geri dönebilir. ControllerBase sınıfından devralınan ve hepsi IActionResult döndüren aşağıda verilen metotlar action'ın vereceği cevabı belirtmek amacıyla kullanılabilir.

Ok(): HTTP 200 kodu gönderilir. Opsiyonel olarak herhangi bir parametre de verilebilir. Bu durumda ilgili nesnenin JSON karşılığı response'a yazılır.
NoContent(): HTTP 204 kodunu gönderir. Talebin başarılı bir şekilde alındığını ve ilgili işlemin yapıldığını ama sonuçta veri göndermeye gerek olmadığını belirtir.
BadRequest(): HTTP 400 kodunu gönderir. Request'in uygun bir formatta yapılmadığına işaret eder. Opsiyonel olarak request'in neden kabul edilmediğini gösterecek bir ModelState nesnesi gönderilebilir.
File(dosya): HTTP 200 kodunu gönderir. Content-Type header'ını dosyanın uzantısına göre belirler ve belirtilen dosyayı istemciye gönderir.
NotFound(): HTTP 404 kodunu gönderir. Aranılan kaynağın bulunamadığını belirtir.
Redirect(url), RedirectPermanent(url): Bu iki metot belirtilen URL'ye yönlendirme mesajı gönderir.
RedirectToRoute(rota), RedirectToRoutePermanent(rota): Yönlendirmeler uygulamanın rota yapılandırması kullanılarak yapılır.
LocalRedirect(url), LocalRedirectPermanent(url): Bu iki metot uygulamanın içinde lokal yönlendirme yapar. Yönlendirme URL ile olur.
RedirectToAction(action), RedirectToActionPermanent(action): Belirtilen action'a yönlendirme yaparlar.
RedirectToPage(page), RedirectToPagePermanent(page): Belirtilen Razor sayfasına yönlendirme yaparlar.
StatusCode(kod): Belirtilen durum kodunu döndürür.

Bir action'ın bu metotları kullanmadan bir nesne döndürmesi Ok() metodunu çalıştırıp parametre olarak ilgili nesneyi vermesine eşdeğerdir. Bir action metodun null değer döndürmesi NoContent() metot çağrısına denktir. Şimdi ProductsController.cs dosyasını şöyle değiştirelim:

using Microsoft.AspNetCore.Mvc;
using WebApp.Models;
namespace WebApp.Controllers
{
    [Route("api/[controller]")]
    public class ProductsController : ControllerBase
    {
        private DataContext context;
        public ProductsController(DataContext ctx)
        {
            context = ctx;
        }
        [HttpGet]
        public IAsyncEnumerable<Product> GetProducts()
        {
            return context.Products.AsAsyncEnumerable();
        }
        [HttpGet("{id}")]
        public async Task<IActionResult> GetProduct(long id)
        {
            Product p = await context.Products.FindAsync(id);
            if (p == null)
            {
                return NotFound();
            }
            return Ok(p);
        }
        [HttpPost]
        public async Task<IActionResult> SaveProduct([FromBody] ProductBindingTarget target)
        {
            Product p = target.ToProduct();
            await context.Products.AddAsync(p);
            await context.SaveChangesAsync();
            return Ok(p);
        }
        [HttpPut]
        public async Task UpdateProduct([FromBody] Product product)
        {
            context.Update(product);
            await context.SaveChangesAsync();
        }
        [HttpDelete("{id}")]
        public async Task DeleteProduct(long id)
        {
            context.Products.Remove(new Product() { ProductId = id });
            await context.SaveChangesAsync();
        }
    }
}

Programımızda iki tane action'da değişiklik yapılmıştır. Artık GetProduct() action'ının aradığı ürün veritabanında yoksa geriye 404 kodunu gönderecektir. Eğer ürün varsa 200 kodunu ve ürünü gönderecektir. Ayrıca SaveProduct() action'ı yeni kaydettiği Product nesnesiyle geri dönmektedir.

Yönlendirmeler

değiştir

Biraz önce gördüğümüz action sonuç metotlarının büyük bir kısmı yönlendirmeler için ayrılmıştır. En basit yönlendirme yöntemi Redirect() metodunu kullanmaktır. Örnek:

[HttpGet("redirect")]
public IActionResult Redirect()
{
    return Redirect("/api/products/1");
}

Bu action uygulamamızın "api/products/1" path'ına yönlendirme yapmaktadır. Bu şekilde yapılan yönlendirmeye statik yönlendirme denir. Uygulamamızın rota mekanizması değiştiğinde yönlendirme çalışmaz hale gelir.

Başka bir action'a yönlendirme yapma

değiştir

Aşağıdaki action aynı controller'daki GetProduct() action'ına yönlendirme yapmaktadır:

[HttpGet("redirect")]
public IActionResult Redirect()
{
    return RedirectToAction(nameof(GetProduct), new { id = 1 });
}

Bu tür bir yönlendirmede uygulamamızın rota yapılanması değiştirilse bile yönlendirme çalışmaya devam edecektir. Eğer örneğimizdeki gibi sadece action'ın belirtildiği RedirectToAction() metodunu kullanırsak controller geçerli controller varsayılır. İstersek controller'ı da belirtebiliriz. Örnek:

[HttpGet("redirect")]
public IActionResult Redirect()
{
    return RedirectToAction(nameof(GetProduct), nameof(Products), new { id = 1 });
}

Rota değişkenlerini belirterek yönlendirme yapma

değiştir

Action'a yönlendirme, arkaplanda yönlendirme için rota değişkenlerini kullanır. MVC'nin rota yapılandırmasında controller ve action isimli iki rota değişkeni bulunur. İstersek bu rota değişkenlerini elle de girebiliriz:

[HttpGet("redirect")]
public IActionResult Redirect()
{
    return RedirectToRoute(new { controller="Products" action="GetProduct", Id = 1 });
}

Yerel yönlendirmeler

değiştir

LocalRedirect() ve LocalRedirectPermanent() metotlarıyla yapılan yerel yönlendirmelerin Redirect() metodu ile yapılan klasik yönlendirmeden tek farkı eğer uygulama içinde olmayan bir URL'ye yönlendirme yapılıyorsa çalışma zamanı hatası fırlatmasıdır.

Kullanıcı tarafından gönderilen veriyi doğrulama

değiştir

Kullanıcı tarafından servisimize gönderilen verilen istediğimiz kriterleri sağlamıyor olabilir. Bu durumda bir doğrulama işlemi yapmamız gerekir. Doğrulama konusu kapsamlı bir konudur, burada sadece bir giriş yapılacaktır. Şimdi ProductBindingTarget.cs sınıfını şöyle değiştirelim:

using System.ComponentModel.DataAnnotations;
namespace WebApp.Models
{
    public class ProductBindingTarget
    {
        [Required]
        public string Name { get; set; };
        [Range(1, 1000)]
        public decimal Price { get; set; }
        [Range(1, long.MaxValue)]
        public long CategoryId { get; set; }
        [Range(1, long.MaxValue)]
        public long SupplierId { get; set; }
        public Product ToProduct() => new Product()
        {
            Name = this.Name,
            Price = this.Price,
            CategoryId = this.CategoryId,
            SupplierId = this.SupplierId
        };
    }
}

Burada ProductBindingTarget sınıfındaki bütün özelliklerin gerekli olduğunu belirttik. Bu kısıtların geçerli olması için ProductsController.cs dosyasındaki SaveProduct() action'ını şöyle değiştirmeliyiz:

[HttpPost]
public async Task<IActionResult> SaveProduct([FromBody] ProductBindingTarget target)
{
    if (ModelState.IsValid)
    {
        Product p = target.ToProduct();
        await context.Products.AddAsync(p);
        await context.SaveChangesAsync();
        return Ok(p);
    }
    return BadRequest(ModelState);
}

ApiController attribute'unun kullanımı

değiştir

Bir controller sınıfına ApiController attribute'unun eklenmesi ilgili controller sınıfının davranışını değiştirir. Bir controller sınıfında ApiController attribute'unun kullanılması actionların parametrelerinde FromBody attribute'unu kullanmasını ve yukarıda if (ModelState.IsValid) şeklinde yaptığınız veri kontrolünü gereksiz kılar. Bu sayede kod daha okunaklı ve sade hale gelir. Bu bilgiler eşliğinde ProductsController.cs dosyamızı şöyle değiştirelim:

using Microsoft.AspNetCore.Mvc;
using WebApp.Models;
namespace WebApp.Controllers
{
    [ApiController]
    [Route("api/[controller]")]
    public class ProductsController : ControllerBase
    {
        private DataContext context;
        public ProductsController(DataContext ctx)
        {
            context = ctx;
        }
        [HttpGet]
        public IAsyncEnumerable<Product> GetProducts()
        {
            return context.Products.AsAsyncEnumerable();
        }
        [HttpGet("{id}")]
        public async Task<IActionResult> GetProduct(long id)
        {
            Product p = await context.Products.FindAsync(id);
            if (p == null)
            {
                return NotFound();
            }
            return Ok(p);
        }
        [HttpPost]
        public async Task<IActionResult> SaveProduct(ProductBindingTarget target)
        {
            Product p = target.ToProduct();
            await context.Products.AddAsync(p);
            await context.SaveChangesAsync();
            return Ok(p);
        }
        [HttpPut]
        public async Task UpdateProduct(Product product)
        {
            context.Update(product);
            await context.SaveChangesAsync();
        }
        [HttpDelete("{id}")]
        public async Task DeleteProduct(long id)
        {
            context.Products.Remove(new Product() { ProductId = id });
            await context.SaveChangesAsync();
        }
        [HttpGet("redirect")]
        public IActionResult Redirect()
        {
            return RedirectToAction(nameof(GetProduct), new { Id = 1 });
        }
    }
}

Örneğimizde SaveProduct() ve UpdateProduct() action'ları oldukça sadeleşmiştir.

Değeri null olan özellikleri çıkarma

değiştir

Uygulamamızda Getproducts() ve GetProduct() action'larının döndürdüğü Product'ların bazı özellikleri daima null olmaktadır. Bunlar Product sınıfına navigasyon amacıyla koyduğumuz Category ve Supplier özellikleridir. Bunlar ileride Entity Framework Core tarafından doldurulacak ve ilgili ürünün hangi kategoride olduğunu ve hangi tedarikçi tarafından sağlandığını kolayca görmemizi sağlayacaktır. Şimdilik değerlerinin null olmasında herhangi bir sakınca yoktur. Ancak ne zaman istemciye bir Product veya Product kolleksiyonu göndersek oluşan JSON'ın içinde değeri daima null olan bu özellikler de olacaktır. Bu sorunun birden fazla çözümü var.

Seçilen özellikleri başka bir tipe projekte etme

değiştir

En basit yaklaşım içinde daima değeri null olacak özellikler barındıran bir tipi değeri daima null olacak bu özellikleri çıkararark başka bir tipe projekte etmektir. Örnek:

[HttpGet("{id}")]
public async Task<IActionResult> GetProduct(long id)
{
    Product p = await context.Products.FindAsync(id);
    if (p == null) {
        return NotFound();
    }
    return Ok(new
    {
        ProductId = p.ProductId,
        Name = p.Name,
        Price = p.Price,
        CategoryId = p.CategoryId,
        SupplierId = p.SupplierId
    });
}

Bu yöntem basittir. Ancak bazı dezavantajları vardır:

  • Sunucu tarafındaki geliştirici anonim tipe eklenecek özellikler bakımından kısıtlanmadığı için servisi tüketecek geliştirici aldığı nesnede hangi özelliklerin olup hangilerinin olmadığı konusunda emin olamaz.
  • Sunucu tarafındaki geliştirici eklenmesi gereken bir veya birden fazla özelliği eklemeyi unutabilir.
  • Sunucu tarafındaki geliştirici her Product nesnesi göndereceği zaman aynı anonim tip tanımını ve projeksiyonunu yapmak zorunda olduğu için kod tekrarı oluşur ve kodun bakımı zorlaşır.

JSON serileştiricisini konfigüre etme

değiştir

Daha güzel bir yaklaşım JSON serileştiricisini konfigüre etmektir. Şimdi Product.cs dosyamızı şöyle değiştirelim:

using System.ComponentModel.DataAnnotations.Schema;
using System.Text.Json.Serialization;
namespace WebApp.Models
{
    public class Product
    {
        public long ProductId { get; set; }
        public string Name { get; set; }
        [Column(TypeName = "decimal(8, 2)")]
        public decimal Price { get; set; }
        public long CategoryId { get; set; }
        public Category Category { get; set; }
        public long SupplierId { get; set; }
        [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
        public Supplier Supplier { get; set; }
    }
}

Product sınıfımızdaki Supplier özelliğine uygulanan attribute sayesinde artık Supplier özelliğinin değeri null ise JSON serileştiricisi tarafından serileştirmeye dahil edilmeyecek. Buradaki JsonIgnoreCondition bir enumdur ve tanımladığı sözcükler şunlardır:

Always: İlgili özellik daima yok sayılır, hiçbir zaman serileştirilmez.
Never: İlgili özellik hiçbir zaman yok sayılmaz, her zaman serileştirilir.
WhenWritingDefault: İlgili özelliğin değeri varsayılan değerse serileştirilmez. Varsayılan değer referans tipleri için null; int, long gibi sayısal temel veri tipleri için 0'dır.
WhenWritingNull: İlgili özelliğin değeri null'sa serileştirilmez.

Şimdi ProductsController sınıfımızdaki anonim tip tanımını ve projeksiyonu kaldırabiliriz:

[HttpGet("{id}")]
public async Task<IActionResult> GetProduct(long id)
{
    Product p = await context.Products.FindAsync(id);
    if (p == null)
    {
        return NotFound();
    }
    return Ok(p);
}

Sınıf bazında yaptığımız bu konfigürasyon sadece Product sınıfının Supplier özelliği için geçerlidir. Eğer programımızda genel olarak JSON serileştiricisinin null değerleri serileştirmesini istemiyorsak Program.cs dosyasında JSON serileştiricisini konfigüre ederiz.Şimdi Program.cs dosyasını şöyle değiştirelim:

using Microsoft.EntityFrameworkCore;
using WebApp.Models;
using Microsoft.AspNetCore.Mvc;
using System.Text.Json.Serialization;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<DataContext>(opts => {
    opts.UseSqlServer(builder.Configuration["ConnectionStrings:ProductConnection"]);
    opts.EnableSensitiveDataLogging(true);
});
builder.Services.AddControllers();
builder.Services.Configure<JsonOptions>(opts => {
    opts.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
});
var app = builder.Build();
app.MapControllers();
app.MapGet("/", () => "Hello World!");
var context = app.Services.CreateScope().ServiceProvider.GetRequiredService<DataContext>();
SeedData.SeedDatabase(context);
app.Run();

JSON serileştiricisi Program.cs dosyası üzerinden bu şekilde konfigüre edildiği zaman DefaultIgnoreCondition özelliği JsonIgnoreCondition.Always olarak ayarlanmamalıdır. Genel JSON Ignore yapılandırmasıyla özellik tabanlı JSON Ignore yapılandırması beraber kullanılabilir. Bu durumda özellik tabanlı JSON Ignore yapılandırması genel JSON Ignore yapılandırmasının üzerine yazacaktır.