ASP.NET Core 6/Web Servisleri/Controller Kullanarak Web Servis Oluşturma

Bu bölümde controller sınıfı ve action metodu kullanarak web servis oluşturacağız.

MVC framework'unun etkinleştirilmesi

değiştir

Web servislerini controller ve action'lar üzerinden tanımlayabilmemiz için öncelikle MVC framework'unu etkinleştirmemiz gerekmektedir. Şimdi Program.cs dosyanızı şöyle değiştirin:

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

Buradaki builder.Services.AddControllers(); satırı MVC framework'u tarafından kullanılan servisleri ayağa kaldırır, app.MapControllers(); satırı ise rota yapılandırmasını MVC framework'una göre yapar.

Controller'ın oluşturulması

değiştir

Action denen metotları içeren sınıflara controller denir. Sunucunun belirli bir path'ına talep gelmesi durumunda ilgili controller sınıfının nesnesi oluşturulur, bu nesne üzerinden ilgili action metodu çağrılır. Gelen request'in path'ına göre hangi controller ve hangi action'ın seçileceği biraz önce eklediğimiz app.MapControllers(); satırı sayesinde MVC Framework'u tarafından belirlenir. Controller sınıfları geleneksel olarak Controllers klasöründe tutulur ve isimlerinin Controller ile bitmesi zorunludur. Şimdi Controllers klsörünü ve bu klasörin içindeki ProductsController.cs sınıfını oluşturun. ProductsController.cs dosyasının içeriği şöyle olsun:

using Microsoft.AspNetCore.Mvc;
using WebApp.Models;
namespace WebApp.Controllers
{
    [Route("api/[controller]")]
    public class ProductsController : ControllerBase
    {
        [HttpGet]
        public IEnumerable<Product> GetProducts()
        {
            return new Product[] { new Product() { Name = "Product #1" }, new Product() { Name = "Product #2" } };
        }
        [HttpGet("{id}")]
        public Product GetProduct()
        {
            return new Product() { ProductId = 1, Name = "Test Product" };
        }
    }
}

Controller'lar Microsoft.AspNetCore.Mvc isim alanındaki ControllerBase sınıfından türerler. Bu controller, eklenen Route attribute'u sayesinde "api/<controller>" path'ına karşılık verecek şekilde yapılandırılmıştır. Controller'ın ismi Products olduğuna göre bu controller "api/products" path'ına karşılık verecektir. Controller'da iki tane action bulunmaktadır. Birisi GET talebine karşılık verecek olan GetProducts() action'ı, diğeri yine GET talebine karşılık verecek olan GetProduct() action'ıdır. GetProduct() action'ı id isimli bir parametre almaktadır. Bu parametre birazdan da göreceğimiz üzere request'te api/products/<id> şeklinde verilmelidir. Controller basit olması açısından veritabanıyla iş yapmamakta, dahili .NET nesneleriyle çalışmaktadır. Bu controller'daki action'lardan birincisi tam olarak "api/products" path'ına karşılık vermekteyken, ikinci action "api/products/<<id>" path'ına karşılık vermektedir.

Web servis oluştururken kullanılan action metotlarına herhangi bir isim verilebilir. Action metodun isminin cevap verdiği path'la bir ilgisi yoktur. Web servislerde bir action metodun hangi path'a ve hangi metoda cevap vereceği yalnızca controller ve action'a atanan attribute'lar vasıtasıyla belirtilir. Action'ların cevap verdiği HTTP metodunun belirtilmesi için kullanılabilecek bütün attribute'lar şunlardır:

HttpGet: İlgili action yalnızca GET request'lerinde tetiklenir.
HttpPost: İlgili action yalnızca POST request'lerinde tetiklenir.
HttpDelete: İlgili action yalnızca DELETE request'lerinde tetiklenir.
HttpPut: İlgili action yalnızca PUT request'lerinde tetiklenir.
HttpPatch: İlgili action yalnızca PATCH request'lerinde tetiklenir.
HttpHead: İlgili action yalnızca HEAD request'lerinde tetiklenir.
AcceptVerbs: Action metodun belirtilen birden fazla HTTP metodunda tetiklenebileceğini belirtir.

Bu attribute'lar string tipinde parametre alabilir. Alınan parametre controller sınıfına eklenen Route attribute'unun belirttiği rota argümanına / karakteri ile eklenir ve metodun cevap verdiği path'ı oluşturur. Örneğimizde sonuç olarak ikinci action'ın cevap verdiği path "/api/products/{id}" olacaktır. id değerinin süslü parantezler içinde olması ilgili path segmentinin bir path değişkeni olacağı anlamına gelir.

ControllerBase sınıfı

değiştir

ControllerBase bütün controller'ların türediği sınıftır ve controller'ın içindeki action'lar tarafından direkt erişilebilecek çok güzel üyelere sahiptir. Bu üyelerden önemli olanları şunlardır:

HttpContext: Geçerli request'ın HttpContext nesnesini döndürür.
ModelState: Forma girilen verilerin validasyonu yapılırken kullanılır. Bu özellik üzerinden erişilen çeşitli özellikler vasıtasıyla forma girilen verilerin geçerli olup olmadığı anlaşılabilir.
Request: Geçerli request'in HttpRequest nesnesini döndürür.
Response: Geçerli request'in response'unu döndürür.
RouteData: Geçerli request'in rota verisini döndürür.
User: Geçerli request'i yapan kullanıcıyı döndürür.

Her request'te yeni bir controller nesnesi oluşturulur, dolayısıyla bu özellikler sadece ilgili request hakkında bilgi verir.

NOT: ASP.NET Core tarafından bir sınıfın controller sınıfı olarak ele alınması için isminin Controller ile bitmesi veya ismi Controller ile biten bir sınıftan türemesi veya Controller attribute'ile işaretlenmesi yeterlidir. Bir sınıfın controller olarak ele alınması için Controllers isimli klasörde tanımlanması, ControllerBase veya Controller sınıfından türemesi gerekli değildir. Controller olma şartlarını taşıyan bir sınıfın controller olmadığını belirtmek için NonController attribute'u ile işaretleriz.

Action'ların geri dönüş değerleri

değiştir

Web servisleri için controller kullanılmasının bir avantajı istemciye gönderilecek nesnelerin elle JSON'a dönüştürülme zorunluluğunu ortadan kaldırmasıdır. Örneğimizde GetProduct() action'ının geri dönüş tipi direkt Product'tır. Bu action'a bir talep gelmesi durumunda otomatik olarak response'a ilgili Product nesnesinin JSON karşılığı yazılacak, header'lar otomatik olarak doldurulacak ve gönderilecektir. Örneğimizde api/products/1 path'ına talep gönderdiğinizde GetProduct() action'ı tetiklenecek, bu action'ın döndürdüğü Product nesnesi otomatik olarak JSON'a çevrilip istemciye gönderilecektir. Benzer şekilde api/products path'ına talepte bulunduğunuzda GetProducts() action'ı tetiklenecek, oluşturulan iki adet Product nesnesinden oluşan dizi bir JSON metni şeklinde serileştirilecek ve istemciye gönderilecektir.

Controller'larda dependency injection'ın kullanımı

değiştir

Controller sınıflarına dependency injection yöntemiyle servis enjekte etmek çok kolaydır. Sunucuya gelen her istekte Controller sınıfı türünden nesne oluşturulup bu nesne üzerinden ilgili action metodunun çalıştırıldığını söylemiştik. İşte controller sınıfının yapıcı metodu parametre olarak bir servis tipi alırsa bu servis otomatik olarak çözümlenir. Ayrıca action'lar da kendilerine servis enjekte edebilirler. Şimdi ProductsController.cs dosyamızı şö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 IEnumerable<Product> GetProducts()
        {
            return context.Products;
        }
        [HttpGet("{id}")]
        public Product GetProduct([FromServices] ILogger<ProductsController> logger)
        {
            logger.LogDebug("GetProduct Action Invoked");
            return context.Products.FirstOrDefault();
        }
    }
}

Örneğimizde hem yapıcı metot hem de action metodu üzerinden servis enjeksiyonu yapılmaktadır. Buradaki servisler request tabanlı olarak HttpContext nesnesi üzerinden enjekte edilir. Dolayısıyla her türlü servise gönül rahatlığıyla erişebiliriz. Bildiğiniz üzere transient ve scoped servislere Program.cs dosyası üzerinden app değişkenini kullanarak erişemiyorduk.

Varsayılan yapılandırmada veritabanı context nesnelerine scoped olarak erişilir. Her request'te oluşturulan controller nesnesinin bağımlı olduğu context nesnesi de her request'te ayrı olarak oluşturulur. Bu oluşturulan context nesnesini request'ler arasında taşımaya çalışmamalı ve başka controller'lara göndermemeliyiz. Bir controller'ın işi bitip response'u döndürdükten sonra context nesnesinin de işi bitmeli.

GetProduct() action'ı ILogger<> servisine bağımlıdır. Dolayısıyla programın çalışabilmesi için Program.cs dosyasında ILogger<> servisine abonelik yapmalısınız.

Bir metoda parametre yoluyla servis enjekte ediliyorsa ilgili parametrenin FromServices attribute'u ile işaretlenmesi gerekir. Bu attribute ilgili parametrenin normal bir parametre olmadığını belirtir. Daha sonra göreceğimiz üzere normal parametreler request'ten veri almaya yararlar.

Model binding kullanımı

değiştir

Model binding daha sonra göreceğimiz ileri bir konudur. Bu bölümde sadece kısa bir giriş yapacağız. Şimdi ProductsController.cs sınıfımızı şö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 IEnumerable<Product> GetProducts()
        {
            return context.Products;
        }
        [HttpGet("{id}")]
        public Product GetProduct(long id, [FromServices] ILogger<ProductsController> logger)
        {
            logger.LogDebug("GetProduct Action Invoked");
            return context.Products.Find(id);
        }
    }
}

Burada GetProduct() metodu, ilk parametresi olan id değişkeninin değerini aynı isimli rota segmentinden almaktadır. Çünkü attribute'unun parametresinde küme parantezleri içine alınmış bir id değişkeni vardır. Bu sayede GetProduct() metodunun gövdesi içinde direkt rota segmentleri ile uğraşma zorunluluğundan kurtuluruz.

Request gövdesinden model binding yapma

değiştir

Yukarıda model binding aracılığıyla bir rota değişkeninin değerini direkt metot içinde kullanabildik. Model binding bundan çok daha fazlasıdır. Model binding ile gelen request'in body'sinden de otomatik .NET nesneleri oluşturulmasını ve bu nesnenin action metodunun içinde kullanılabilir olmasını sağlayabiliriz. Üstelik bunu etkileştirmek için hiçbir şey yapmamıza gerek yok. Sadece metodun aldığı parametre tipini ilgili karmaşık tip yapmamız yeterli. Ş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 IEnumerable<Product> GetProducts()
        {
            return context.Products;
        }
        [HttpGet("{id}")]
        public Product? GetProduct(long id, [FromServices] ILogger<ProductsController> logger)
        {
            logger.LogDebug("GetProduct Action Invoked");
            return context.Products.Find(id);
        }
        [HttpPost]
        public void SaveProduct([FromBody] Product product)
        {
            context.Products.Add(product);
            context.SaveChanges();
        }
    }
}

Burada yapılan ekleme SaveProduct() action'ıdır. Bu action, "api/products" path'ına bir POST request'i geldiğinde tetiklenecektir. FromBody attribute'u ilgili parametrenin request'i body'siyle doldurularacağı belirtmektedir. SaveProduct() action'ına bir talep geldiğinde request'in body'sindeki JSON deserialize edilerek Product nesnesine otomatik olarak dönüştürülür. Sonra ilgili Product nesnesi metot gövdesinde istenildiği şekilde kullanılabilir.

Ek action'lar ekleme

değiştir

Şimdi ProductsController.cs sınıfı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 IEnumerable<Product> GetProducts()
        {
            return context.Products;
        }
        [HttpGet("{id}")]
        public Product GetProduct(long id, [FromServices] ILogger<ProductsController> logger)
        {
            logger.LogDebug("GetProduct Action Invoked");
            return context.Products.Find(id);
        }
        [HttpPost]
        public void SaveProduct([FromBody] Product product)
        {
            context.Products.Add(product);
            context.SaveChanges();
        }
        [HttpPut]
        public void UpdateProduct([FromBody] Product product)
        {
            context.Products.Update(product);
            context.SaveChanges();
        }
        [HttpDelete("{id}")]
        public void DeleteProduct(long id)
        {
            context.Products.Remove(new Product() { ProductId = id });
            context.SaveChanges();
        }
    }
}

Bu örneğimizde controller sınıfımıza iki yeni action eklenmiştir. Eklenen action'lardan biri var olan kaydı güncellemekte, ikincisi silmektedir.