.NET Core Web API ve JWT Token

Part Three: Security in React and WebApi in ASP.NET Core C# with  authentication and authorization by KeyCloak | by Nicolas Barlatier | Dev  Genius

Merhaba, bu yazımızda API uygulamaları için önemli bir konu olan güvenlik konusunu inceleyeceğiz.

Geliştirdiğimiz API uygulamalarının güvenli olması hem verilerin korunması hem de istenmeyen kişilerin oluşturduğumuz API uygulamalarını kendi amaçları için kullanmasının önünce geçmektedir. Genel olarak baktığımızda uygulamamızın uygun şekilde çalışmasından sonra gelen en önemli konulardan biridir güvenlik.

Bugün, API uygulamalarında güvenlik yöntemlerinden biri olan JWT Token yapısını inceleyeceğiz. Yapıyı, oluşturulmasını ve çalışma yöntemlerini kendimce anlatmaya çalışacağım.

Kimlik Doğrulama (Authentication)

Authentication, bir kullanıcının kimlik doğrulama işlemidir. Bu işlemde kullanıcı, kendisini doğrulayan bir kimlik bilgisi ile uygulama üzerinde yetki alarak kullanımını sağlar. Örneğin bir kullanıcı adı ve şifre ile kullanıcı uygulamaya kendini tanıtır. Bu kimlik bilgileri doğrulandıktan sonra kullanıcının kimliği onaylanır ve sistemdeki kaynaklara erişmesine izin verilir.

.NET Core Web API yapısında, kimlik doğrulama için kullanılacak birçok seçenek bulunmaktadır. Bunlar arasında en yaygın kullanılanı JWT (JSON Web Token) tabanlı kimlik doğrulama yapısıdır. JWT, kullanıcının kimlik bilgilerinin tutulduğu bir veri yapısıdır ve bu veri yapısının doğruluğu, içinde yer alan özel bir anahtar tarafından sağlanır. Bu nedenle, bir JWT kullanarak kimlik doğrulama işlemi gerçekleştirmek oldukça güvenlidir.

JSON Web Token (JWT) nedir ?

JSON Web Token (JWT), web uygulamaları için güvenli bir yetkilendirme yöntemidir. JWT, kullanıcı yetkilendirmesi ile ilgili bilgileri içeren token şeklinde verileri taşır ve bu verileri kullanarak uygulamalar arasında güvenli bir şekilde veri akışı sağlar.

JWT, bir header, bir payload ve bir signature olmak üzere üç parçadan oluşur. Bu parçalar “.” işareti ile ayrılarak gösterilir.

Bu bölümlerin kısaca ne anlama geldiklerine bakalım.

Başlık (Header) :
Başlık bilgisi iki bölümden oluşur. Belirte türü ve imzalama algoritması. İmzalama algoritması HMAC, SHA256 veya RSA olabilmektedir.

Yük (Payload) :
JWT içinde saklanacak veya gönderilecek olan bilgiyi temsil etmektedir. Burada kullanıcı bilgisi, kullanıcı yetki bilgileri veya ek bilgiler yer alabilmektedir.

Payload içerisinde üç tip bilgi bulunabilmektedir. Bunlar;

  • Kayıtlı Talepler (Registered Claims) :
    Gerekli olmayan fakat token ile ilgili bilgileri içeren başlıklardır. Örnek vermek gerekirse sub (subject), iss (issuer) gibi.
  • Açık Talepler (Public Claims) :
    Token’ı kullananlar tarafından eklenebilen bilgiler.
  • Özel Talepler (Private Claims) :
    Token ile aktarılacak veya paylaşılacak bilgileri içermektedir.

Talepler içinde kullanılacak olan bilgilerin rezerve edilmiş bilgilere denk gelmemesi gerekmektedir. Bunun için belirli talep isimlendirme standartları belirlenmiştir.

Kullanılan bilgilere ve kısaltmalara buradan ulaşabilirsiniz. -> https://www.iana.org/assignments/jwt/jwt.xhtml

İmza (Signature) :
İmza bölümünü oluşturmak için Base64Url olarak kodlanmış başlık (header), yük (payload) bilgisini ve bir gizli değer (secret) alınarak başlıkta belirtilen algoritma ile imzalamanız gerekmektedir.

Bu işlemlerden sonra oluşacak olan JWT token bilgisi aşağıdaki gibi olacaktır.

EyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.

EyJzdWIiOiJCeU9udXIiLCJuYW1lIjoiT251ciBLQVJBS1XFniIsImlhdCI6MTUxNjIzOTAyMn0.8rPL9B1HFX-5QGX7qHOIIIsmPyUkFHbigg06sE3U-Pk

https://jwt.io/#debugger-io adresinde JWT token oluşturma ve bu token bilgilerini tekrar açmak ile ilgili debugger uygulamasını kontrol edebilirsiniz.

Her yerde karşılaşılan bildik JWT Token diyagramı.

Kısaca anlatmak gerekirse JWT, bir header, bir payload ve bir signature olmak üzere üç parçadan oluşur. Header, JWT’nin tipini ve encoding formatını tanımlar. Payload, yetkilendirme bilgilerini içeren verilerdir ve bu veriler genellikle JSON formatında tutulur. Signature, header ve payload verilerinin hash değerini içerir ve JWT’nin doğruluğunu kontrol etmek için kullanılır.

JWT, sunucu tarafında veri saklama ihtiyacı olmadan kullanıcı yetkilendirmesi yapılmasına olanak tanır. Bu, uygulamalar arasında güvenli veri transferi yaparken sunucu tarafında veri saklama gereksinimini ortadan kaldırır ve aynı zamanda sunucunun performansını da arttırır.

JWT’nin avantajları arasında, verilerin kolayca okunabilir ve kodlanabilir olması, sunucu tarafında veri saklama gereksinimi olmaması, uygulamalar arasında güvenli veri akışı sağlaması gibi unsurlar bulunur.

Kimlik doğrulama (Authentication) ve yetki (Authorization) nedir?

Kimlik doğrulama ve yetkilendirme, web uygulamalarının güvenliği için iki temel kavramdır.

Kimlik doğrulama (Authentication), daha önce de belirttiğim gibi kullanıcının kimliğini doğrulama işlemidir. Bu işlem, kullanıcının kimliğini kanıtlamasını gerektirir. Örneğin, bir kullanıcının kullanıcı adı ve şifre gibi kimlik bilgileri kullanılarak doğrulanması işlemi kimlik doğrulama (authentication) olarak adlandırılır. Bu işlem sonucunda kullanıcının kimliği doğrulanır ve uygulama tarafından yetkilendirme işlemi yapılabilmesi için uygulamanın da daha sonra anlayabileceği bir token oluşturulur.

Yetkilendirme (Authorization) ise, kullanıcının yapabileceği işlemleri belirleme işlemidir. Kullanıcının uygulama tarafından kimliği doğrulandıktan sonra yapılması gereken işlemdir. Örnek olarak, kullanıcı sadece belirli bir role ait işlemlerin yapılması için yetkilendirilebilir. Yönetici (admin) rolüne sahip bir kullanıcının uygulama içerisinde bu role ait işlemleri yapabilmesi, farklı rollerde olan işlemleri gerçekleştirmek istediği zaman ise belirli bir mesaj ile uyarılması sağlanabilmektedir.

Token-based authentication’ın avantajları

JWT Authentication, birçok web uygulamasında tercih edilen bir kimlik doğrulama yöntemidir. Bu yöntem, kullanıcıların web uygulamanıza kayıt olmaları veya giriş yapmaları için gereken bir parola veya benzeri bilgileri kullanmak yerine, kullanıcının sahip olduğu bir token kullanarak kimlik doğrulama işlemini gerçekleştirir.

Genel olarak web uygulamalarında kullanılan Token-based authentication ölçeklenebilir, performanslı ve güvenli bir kimlik doğrulama yöntemidir.

  • Durumsuz (Stateless) : Sunucu tarafında durum bilgisi tutulmasına gerek kalmadan token kullanıcı tarafından uygulamaya gönderilebilir ve işlenebilir. Bu sebepten dolayı hızlı ve ölçeklenebilir bir yapıya sahiptir.
  • Güvenli (Trustworthy) : Token bilgileri kullanıcı ve uygulama arasında genellikle şifrelenir ve gizli tutulur. Bu sebeple güvenli bir yapıya sahiptir. Token’lar, uygulama tarafından işlenmeden önce, veri bütünlüğü ve doğruluğu sağlamak için imzalanır.
  • Esnek (Flexible) : Token-based authentication platform bağımsız olarak kullanılabilmektedir. Bu da yeni gelişen teknoloji ve uygulama yapıları ile rahat çalışma olanağı sağlamaktadır.
  • Performans (Performance) : Token-based authentication yapısını kullanan uygulamalar gelen her kullanıcı isteği için bir kimlik doğrulama işlemi yapmaz. Bunun yerine gönderilen token bilgisinin doğruluğu kontrol edilir. Bu da uygulamalarımızın daha performanslı ve hızlı çalışmasının sağlar.
  • Ölçeklenebilir (Scalable) : Token-based authentication, sunucular arasında yük dengeleme yaparken de kullanışlıdır. Token’lar sayesinde, yük dengeleme işlemi kolaylaşır ve her sunucu aynı kimlik doğrulama ve yetkilendirme işlemini yapabilir.
  • Özelleştirilebilir (Customizable) : Kullanıcılara özelleştirilmiş izinler ve yetkiler verme işlemini kolaylaştırır. Token’lar, kullanıcılara özel izinlerle birlikte verilebilir ve uygulamanın ihtiyacına göre özelleştirilebilir.
  • Düşük maliyetli (Cost-Effective) : Token’lar genellikle açık kaynaklı kütüphanelerle kolayca oluşturulabilir ve yönetilebilir. Böylelikle uygulama için kimlik doğrulama ve yetkilendirme işlemlerini uygun maliyetli bir şekilde çözüme kavuşturur.

Önemli noktalar bir tanesi kimlik doğrulama işleminin tek başına bir yetkilendirme mekanizması olmamasıdır. Kullanıcıların doğrulanmasının ardından yetki yapısı içerisinde uygulamamızı kullanacak seviyelerin belirlenmesi gerekmektedir. Böylelikle uygulamamız daha güvenli bir hale gelecektir.

.NET Core Web API uygulaması ve JWT Token

Örnek olarak hazırlayacağımız API uygulamamızın biraz yapısından bahsederek başlayalım.

API içerisinde iki farklı controller sınıfımız olacak. Bu sınıfların ilki kullanıcıların giriş işlemlerini gerçekleştirebilecekleri AuthContoller bir diğeri ise kullanıcıların kitap bilgilerine ulaşabilecekleri BookController.

BookController içerisinde iki metot hazırlayacağız ve bu metotların biri giriş işlemini yapmış kullanıcılar için diğeri ise tüm kullanıcılar için çalışacak.

Hızlı bir örnek olması açısından bir veri tabanı üzerinde kayıt tutmak yerine kullanıcı bilgilerini, dönecek olan cevap bilgilerini hard-coded olarak uygulama içerisinde (In-Memory) tutacağız. Tabii burada kullanıcı yönetimi için ASP.NET Identity veya başka bir kimlik doğrulama yapısı kullanarak da kullanıcıların kimlik doğrulama ve yetkilendirme işlemlerini yapabilirsiniz.

Öncelikle projemizi oluşturalım. JwtAuthForBooks isimli bir ASP.NET Core Web Api projesi oluşturuyoruz.

Uygulama kodumuzda hazır olarak gelen Contorllers/WeatherForecastController.cs ve WeatherForecast.cs sınıflarını silebiliriz.

İlk önce kullanıcıların girişlerini yapacağımız Model, Controller, Interface ve Service sınıflarını geliştirelim.

Models isimli bir klasör ile birlikte UserLoginRequest.cs ve UserLoginResponse.cs isimli iki sınıf oluşturuyoruz.

//UserLoginRequest.cs 

namespace JwtAuthForBooks.Models;

public class UserLoginRequest
{
public string Username { get; set; }
public string Password { get; set; }
}
//UserLoginResponse.cs

namespace JwtAuthForBooks.Models;

public class UserLoginResponse
{
public bool AuthenticateResult { get; set; }
public string AuthToken { get; set; }
public DateTime AccessTokenExpireDate { get; set; }
}

Interfaces isminde bir klasör oluşturarak IAuthService.cs isminde bir sınıf oluşturuyoruz. Burada Controller sınıfımıza ekleyebilmek için (inject) servis ara yüzümüzü tanımlayacağız.

//IAuthService.cs

using JwtAuthForBooks.Models;

namespace JwtAuthForBooks.Interfaces;

public interface IAuthService
{
public Task<UserLoginResponse> LoginUserAsync(UserLoginRequest request);
}

Services isminde bir klasör oluşturarak AuthService.cs isminde bir sınıf oluşturuyoruz. Bu sınıf ise bizim login işlemlerimizi yapacak olan servis olacaktır.

//AuthService.cs

using JwtAuthForBooks.Interfaces;
using JwtAuthForBooks.Models;

namespace JwtAuthForBooks.Services;

public class AuthService : IAuthService
{
public Task<UserLoginResponse> LoginUserAsync(UserLoginRequest request)
    {
        UserLoginResponse response = new();

if (string.IsNullOrEmpty(request.Username) || string.IsNullOrEmpty(request.Password))
        {
throw new ArgumentNullException(nameof(request));
        }

if (request.Username == "onur" && request.Password == "123456")
        {
            response.AccessTokenExpireDate = DateTime.UtcNow;
            response.AuthenticateResult = true;
            response.AuthToken = string.Empty;            
        }

return Task.FromResult(response);
    }
}

Controllers klasörümüz içerisine AuthController.cs isminde bir Api Controller oluşturup aşağıdaki gibi düzenliyoruz. Bu sınıf bizim login işlemlerimizi yaptığımız controller olacaktır.

//AuthController.cs

using JwtAuthForBooks.Interfaces;
using JwtAuthForBooks.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

namespace JwtAuthForBooks.Controllers;

[Route("api/[controller]")]
[ApiController]
public class AuthController : ControllerBase
{
readonly IAuthService authService;

public AuthController(IAuthService authService)
 {
this.authService = authService;
 }

    [HttpPost("LoginUser")]
    [AllowAnonymous]
public async Task<ActionResult<UserLoginResponse>> LoginUserAsync([FromBody] UserLoginRequest request)
    {
var result = await authService.LoginUserAsync(request);

return result;
    }
}

Servisin aktif olması için Program.cs sınıfı içerisinde servisimizi kayıt ediyoruz.

//Program.cs

using JwtAuthForBooks.Interfaces;
using JwtAuthForBooks.Services;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.

builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

//Eklenecek olan kayıt satırı.
builder.Services.AddTransient<IAuthService, AuthService>();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseAuthorization();

app.MapControllers();

app.Run();

Uygulamamızı çalıştırdığımız zaman controller üzerinden login işlemini yapabiliyoruz. Burada token ve tarih bilgileri boş olarak geliyor. Birazdan JWT token ile ilgili geliştirmelerimizi ve kullanacağımız BookController sınıfımızı ekleyeceğiz.

Öncelikle JWT işlemleri için kullanacağımız Nuget paketini ekleyelim. Aşağıdaki paket ismini bulabilirsiniz. .NET 6 ile projemizi oluşturduğumuz için 6.0.14 versiyonunu almamız gerekmektedir. Eğer siz .NET 7 ile projenizi oluşturduysanız 7 versiyonunu alabilirsiniz.

Microsoft.AspNetCore.Authentication.JwtBearer

Token işlemleri yapmak için kullanacağımız model, servis ara yüzü ve servis sınıfını geliştirelim. Model bilgileri Models , ara yüz bilgileri Interfaces, servis sınıfı ise Services klasörü içinde olacaktır.

//GenerateTokenRequest.cs

namespace JwtAuthForBooks.Models;

public class GenerateTokenRequest
{
public string Username { get; set; }
}
//GenerateTokenResponse.cs

namespace JwtAuthForBooks.Models;

public class GenerateTokenResponse
{
public string Token { get; set; }
public DateTime TokenExpireDate { get; set; }
}
//ITokenService.cs

using JwtAuthForBooks.Models;

namespace JwtAuthForBooks.Interfaces;

public interface ITokenService
{
public Task<GenerateTokenResponse> GenerateToken(GenerateTokenRequest request);
}
//TokenService.cs

using JwtAuthForBooks.Interfaces;
using JwtAuthForBooks.Models;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;

namespace JwtAuthForBooks.Services;

public class TokenService : ITokenService
{
readonly IConfiguration configuration;

public TokenService(IConfiguration configuration)
    {
this.configuration = configuration;
    }

public Task<GenerateTokenResponse> GenerateToken(GenerateTokenRequest request)
    {
        SymmetricSecurityKey symmetricSecurityKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(configuration["AppSettings:Secret"]));

var dateTimeNow = DateTime.UtcNow;

        JwtSecurityToken jwt = new JwtSecurityToken(
                issuer: configuration["AppSettings:ValidIssuer"],
                audience: configuration["AppSettings:ValidAudience"],
                claims: new List<Claim> {
new Claim("userName", request.Username)
                },
                notBefore: dateTimeNow,
                expires: dateTimeNow.Add(TimeSpan.FromMinutes(500)),
                signingCredentials: new SigningCredentials(symmetricSecurityKey, SecurityAlgorithms.HmacSha256)
            );

return Task.FromResult(new GenerateTokenResponse
        {
            Token = new JwtSecurityTokenHandler().WriteToken(jwt),
            TokenExpireDate = dateTimeNow.Add(TimeSpan.FromMinutes(500))
        });
    }
}

Son olarak da appsettings.json dosyasında da aşağıdaki değişiklikleri yapmamız gerekiyor.

//appsettings.json

{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",

"AppSettings": {
"ValidAudience": "AudienceInformation",
"ValidIssuer": "IssuerInformation",
"Secret": "JWTAuthenticationHIGHsecuredPasswordVVVp1OH7Xzyr",
}
}

Login metodumuzu yeniden çalıştırdığımız zaman JWT Token bilgisinin oluştuğunun görebileceğiz.

Burada durup biraz servisi incelememiz gerekiyor. Bu servis içerisinde bulunan GenerateToken metodunda kullanıcının girdiği kullanıcı adını da kullanarak bir JWT token oluşturuyoruz.

JwtSecurityToken sınıfı bizim için JWT token için gerekli bilgileri alarak token oluşturma işlemini gerçekleştiriyor. Peki, bu bilgilerin neler olduğunun incelemeye çalışalım.

Önemli noktalardan biri JwtSecuritToken sınıfına signingCredentials olarak geçtiğimiz Secret değeri. Bu değer sayesinde token bilgimizi imzalayabiliyoruz. Bu imzalama işlemini de appsetting.json dosyamız içinde belirlediğimiz değerden bir SymmetricSecurityKey oluşturarak yapıyoruz. Bu değeri bize gönderilen token üzerinden alarak kontrol edebiliriz ve bizim tarafımızdan imzalandığını anlayabiliriz.

Burada dikkat edilmesi gereken konu token bilgileri okunabilir yapılardır. Bundan dolayı özel bilgilerin token içinde olmaması gerekmektedir.

issuer bilgisi token değerinin kimin tarafından dağıtıldığını yani bizi belirten bir değerdir. Örnek olarak MyBookStore gibi bir değer belirtilebilir.

audience oluşturulacak olan token değerinin kimler tarafından kullanılacağını belirler. Bir site (www.test.com) için üretilecek olan token bilgisi olabilir.

expires Token bilgisinin ne kadar süre ile aktif olacağını belirler. Bu süre sonrasında token kullanılmaz halde olacak ve Api metotlarının kullanımları sırasında yetki hatası verilecektir.

notBefore Token bilgisi üretildikten belirli bir zaman sonra devreye girmesini istersen burada bir değer geçip bu özelliği aktif edebiliriz. Biz token bilgisinin hemen aktif olmasının istediğimiz için üretildiği zamanı başlama değer olarak belirledik.

claims Token bilgisi içinde saklamak istediğimiz bilgileri eklediğimiz bölüm olarak tanımlanabilir. Burada özel olmayacak şekilde istediğimiz bir bilgiyi token içinde tanımlayıp daha sonra bize bu token gönderildiği zaman bu değerleri alarak belirli işlemler yapabiliriz.

Şimdi de yetkilendirme testini yapacağımız BookController sınıfını geliştirelim. Ardından uygulamamız için kimlik doğrulamayı aktif edeceğiz ve testimizi yapabileceğiz.

Senaryomuzda dhaa önce belirttiğimiz gibi iki adet controller metodumuz olacak. bu metotlardan birisi kitap listesini dönecek ve tüm kullanıcılar için çalışabilecek. İkinci metodumuz ise sadece login işlemini başarılı bir şekilde yapmış ve token bilgisini gönderen yetkili kullanıcılar için çalışacak ve kitap bilgisini bize dönecek.

Models klasörümüze BookInformation.cs, BookTitle.cs ve GetBookInformationByIdRequest.cs isminde üç sınıf oluşturarak başlıyoruz.

//BookInformation.cs

namespace JwtAuthForBooks.Models;

public class BookInformation
{
public int BookId { get; set; }
public string Isbn { get; set; }
public string Title { get; set; }
public string Description { get; set; }
public string AuthorName { get; set; }
}
//BookTitle.cs

namespace JwtAuthForBooks.Models;

public class BookTitle
{
public int BookId { get; set; }
public string Isbn { get; set; }
public string Title { get; set; }
}
//GetBookInformationByIdRequest.cs

namespace JwtAuthForBooks.Models;

public class GetBookInformationByIdRequest
{
public int BookId { get; set; }
}

Servis düzenimizi bozmayalım. Interfaces klasörümüze IBookService.cs ve Services klasörümüze BookService.cs isminde iki sınıf oluşturuyoruz ve aşağıdaki gibi düzenliyoruz.

//IBookService.cs

using JwtAuthForBooks.Models;

namespace JwtAuthForBooks.Interfaces;

public interface IBookService
{
public Task<List<BookTitle>> GetBookTitlesAsync();
public Task<BookInformation> GetBookInformationByIdAsync(GetBookInformationByIdRequest request);
}
//BookService.cs

using JwtAuthForBooks.Interfaces;
using JwtAuthForBooks.Models;

namespace JwtAuthForBooks.Services;

public class BookService : IBookService
{
readonly List<BookInformation> bookInformations;

public BookService()
    {
        bookInformations = new List<BookInformation> {
new BookInformation { BookId = 1, Isbn = "9752115047", Title ="22/11/63",  AuthorName = "Stephen King",  Description = "22 Kasım 1963’te, bütün bunları değiştirme şansınız olsaydı?" },
new BookInformation { BookId = 2, Isbn = "1476762740", Title ="Uyuyan Güzeller",  AuthorName = "Stephen King *  Owen King",  Description = "Şimdi burada dünyanın kaderine karar verilecek." },
new BookInformation { BookId = 3, Isbn = "9752126049", Title ="Enstitü",  AuthorName = "Stephen King",  Description = "Enstitü..." }
        };
    }

public Task<BookInformation> GetBookInformationByIdAsync(GetBookInformationByIdRequest request)
    {
var loadedBookInformation = bookInformations.FirstOrDefault(p => p.BookId == request.BookId);

return Task.FromResult(loadedBookInformation);
    }

public Task<List<BookTitle>> GetBookTitlesAsync()
    {
var booktitleList = bookInformations.Select(book => GenerateBookTitleForList(book)).ToList();

return Task.FromResult(booktitleList);
    }

private static BookTitle GenerateBookTitleForList(BookInformation book)
    {
return new BookTitle { BookId = book.BookId, Title = book.Title, Isbn = book.Isbn };
    }
}

Şimdi de BookController.cs sınıfımızı Controllers klasörüne ekliyoruz.

using JwtAuthForBooks.Interfaces;
using JwtAuthForBooks.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

namespace JwtAuthForBooks.Controllers;

[Route("api/[controller]")]
[ApiController]
public class BookController : ControllerBase
{
readonly IBookService bookService;

public BookController(IBookService bookService)
 {
this.bookService = bookService;
 }

    [HttpPost("GetBookTitles")]
    [AllowAnonymous]
public async Task<ActionResult<List<BookTitle>>> GetBookTitles()
    {
var result = await bookService.GetBookTitlesAsync();

return result;
    }

    [HttpPost("GetBookInformationById")]
    [Authorize]
public async Task<ActionResult<BookInformation>> GetBookInformationById([FromBody] GetBookInformationByIdRequest request)
    {
var result = await bookService.GetBookInformationByIdAsync(request);

return result;
    }
}

Burada dikkat edilmesi gereken konu tüm kullanıcıların yetki almadan kullanacakları metodumuza [AllowAnonymous], yetkili olarak kullanılacak metodumuza ise [Authorize] özelliklerini tanımlıyoruz. İsimlerinden de anlaşılacağı gibi anonim ve yetkili çalışacak metotlarımızı bu şekilde belirtmiş olduk.

Şimdi de Program.cs sınıfımıza servisimizi kayıt edelim.

//Program.cs

using JwtAuthForBooks.Interfaces;
using JwtAuthForBooks.Services;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.

builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

builder.Services.AddTransient<IAuthService, AuthService>();
builder.Services.AddTransient<ITokenService, TokenService>();

//Eklenecek olan kayıt satırı.
builder.Services.AddTransient<IBookService, BookService>(); 

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseAuthorization();

app.MapControllers();

app.Run();

Tüm geliştirmeleri yaptığımıza göre şimdi de uygulamamız için yetkilendirme yapısının aktif edelim. Bunun için yine Program.cs sınıfımız içinde tanımlamalarımızı yapalım.

//Program.cs

using JwtAuthForBooks.Interfaces;
using JwtAuthForBooks.Services;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using System.Text;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.

builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

builder.Services.AddTransient<IAuthService, AuthService>();
builder.Services.AddTransient<ITokenService, TokenService>();
builder.Services.AddTransient<IBookService, BookService>();

//Eklenecek olan satırlar
builder.Services.AddAuthentication(options =>
{
    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(o =>
{
    o.TokenValidationParameters = new TokenValidationParameters
    {
        ValidIssuer = builder.Configuration["AppSettings:ValidIssuer"],
        ValidAudience = builder.Configuration["AppSettings:ValidAudience"],
        IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["AppSettings:Secret"])),
        ValidateIssuer = true,
        ValidateAudience = true,
        ValidateLifetime = false,
        ValidateIssuerSigningKey = true
    };
});

builder.Services.AddAuthorization();

//Eklenecek olan satırların sonu

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

//Eklenecek olan satırlar

app.UseAuthentication();
app.UseAuthorization();

//Eklenecek olan satırların sonu

app.MapControllers();

app.Run();

Projemizin son durumu aşağıdaki gibi olacaktır.

AddAuthentication() tanımı ile JWT token yapısının çalışmasını tanımladık. AddAuthorization() ile yetkilendirmenin olacağınız belirttik. UseAuthentication() ve UseAuthorization() ile de bu tanımları aktif ettik.

Metotlarımızı test edebiliriz. Test sırasında [AllowAnonymous] olarak tanımlı metodumuz sorunsuz bir şekilde çalışacaktır.

Yalnız, login işlemini başarılı bir şekilde geçmemiş ve token bilgisini almamış kullanıcılar GetBookInformationById metodunu çalıştırdıkları zaman 401 Unauthorized hatası alacaklardır.

Login metodu üzerinden kullanıcı adı ve şifre ile login olup token bilgisini alalım. Ardından GetBookInformationById metoduna bu token bilgisini göndererek işlemi tekrar deneyeceğiz.

Başarılı bir şekilde giriş işlemini yaptık ve token bilgisini aldık.

Şimdi bu token bilgisini GetBookInformationById metoduna göndererek işlemi yenidne deneyeceğiz. Yalnız, burada token gönderirken header bilgileri içerisinde Authorization key değeri ile bunu ileteceğiz. Bearer <token_bilgisi> şeklinde.

Ardından metodumuzu yeniden çalıştırdığımızda yetkimizi almış bir şekilde metot cevabının geldiğini görebiliriz.

Bu yazıda JWT Token konusunu temel olarak dilim döndüğünce anlatmaya çalıştım. Umarım yararlı bilgiler arasına girer sizler için.

Oluşturduğumuz uygulama kodlarına aşağıdaki bağlantıdan ulaşabilirsiniz.

https://github.com/onurkarakus/JwtAuthForBooks

Görüşmek üzere.

.NET 6 Web API, CQRS ve MediatR kütüphanesi

Merhaba, bu yazıda beraber CQRS (Command Query Responsibility Segregation) ve Mediator tasarım prensipleri ve ile ilgili çalışmalar yapacağız.

MediatR kütüphanesi iki temel prensip üzerinde kurulmuş bir kütüphanedir. Bunlar; CQRS ve Mediator.

Projenin kodlarına aşağıdaki bağlantıdan ulaşabilirsiniz. https://github.com/onurkarakus/CqrsMediatorPattern

Öncelikle CQRS nedir? biraz onu anlatmaya çalışacağım.

CQRS adından da anlaşıldığı gibi komut ve sorguların ayrılması prensibidir.
Genel olarak kullandığımız yapılarda CRUD işlemlerini aslında tek bir repository üzerinden yönetmeye çalışıyoruz. CQRS bu yapıyı bölme üzerine kurulu bir tasarım mantığı getiriyor.

Komut nesneleri veriler üzerinden değişiklik yapan (CUD), sorgular ise veriler üzerinde okuma işlemini (R) gerçekleştiren nesneler olarak tanımlanmaktadır.

CQRS prensibinin çalışma mantığı

Gördüğünüz gibi bu prensip uygulama içinde okuma ve komut işlemlerinin tamamen ayrılmasını destekler. Burada ayırma işlemi için belirli bir sınır veya yöntem kesin olarak belirtilmemiştir. İstenildiği zaman bir model, uygulama veya veri tabanı bazında da ayırma işlemi yapılabilir. Tabii ki bu kurallar uygulamanın boyutu ve karmaşıklığına göre değişebilmektedir.

Bu prensibin kısa tanımı ve temel prensibi aşağıdaki gibi tanımlanabilir.

“Bir metot bir nesnenin durumunu değiştirmelidir veya bir değer döndürmelidir. İkisini birlikte yapmamalıdır.”

Avantajları :

  • Uygulama içerisinde kullanıcılara verileri sunmak için kullanacağımız ViewModel nesnelerinin veri modellerinden oluşturulması (map) işlemlerinde karmaşıklığın önüne geçmektedir.
  • Çok sayıda veri tabanı işlemi içeren bir uygulama tasarlayacaksanız performansa önem vermek zorunda kalacaksınız demektir. Böyle bir durumda okuma ve yazma işlemlerinin ayrı nesneler olması bu işlemlerin ayrı ayrı kontrol altına alınması, yönetilmesi ve performans çalışmalarının yapılması anlamına gelmektedir.
  • Yazma ve okuma işlemleri ayrı şekilde ölçeklenebilir olduğundan dolayı daha az işlem kilitlenmeleri (db lock vb.) olacaktır.
  • Uygulama kodlarının açık ve okunabilir olmasını sağlar.
  • Takıma yeni başlayacak olan yazılımcıların projeye adaptasyonunu hızlandırabilmektedir.

Dezavantajları :

  • Eğer küçük ölçekli bir uygulama geliştirecekseniz CQRS prensibinin uyarlaması sizin için yorucu olacaktır.
  • Sadece CQRS prensibini kullanmak çok fazla kod geliştirme ihtiyacı oluşturacağından dolayı projenin geliştirme süreleri uzayacaktır.

Peki Mediator nedir ?

Mediator Tasarım Deseni nesneler arasındaki bağımlılıkları azaltmamız için bizlere olanak sağlayan bir prensiptir. En rahat kullanılan sınıf bağımlılıkları en az olan sınıftır mantığından yola çıkarak bu tasarım kalıbı ile kullanacağımız nesnelerin aynı ara yüzü uyarlar. Bu prensip ise nesnelerin iletişimlerinin birbirleri ile değil bu ara yüz üzerinden yapmalarını sağlamayı amaçlar.

En rahat kullanılan sınıf bağımlılıkları en az olan sınıftır mantığından yola çıkarak bu tasarım kalıbı ile kullanacağımız nesnelerin aynı ara yüzü uyarlaması ile iletişimlerinin birbirleri ile değil bu ara yüz üzerinden yapmalarını sağlamayı amaçlar.

Bu desen sayesinde kullandığımız nesneler birbirleri bağımlı olmak yerine iletişimi bir aracı ile sağlarlar.

Genel olarak araştırdığınız zaman verilen örneklerin en meşhuru havaalanı, kula ve uçak örneğidir. Uçakların zorunlu olmadıkları sürece birbirleri ile konuşmayarak kule ile konuşmaları bu prensibe basit bir örnek olabilir.

MediatR kütüphanesi ise Mediator tasarım desenini uygulamamız için bize kolaylık sağlayan bir kütüphanedir.

Şimdi bir proje üzerinden önce CQRS tasarım deseni örneğini incelemeye çalışalım. Ardından da MediatR kütüphanesi ile projemizi Mediator tasarım deseni ile daha kullanışlı hale getirelim.

Yeni bir ASP.NET Core Web API projesi oluşturarak isimlendirelim.

CqrsMediatorPattern isimli proje için oluşturma ekranı

Projemiz ilk oluştuğu zaman hazır olacak gelen dosyaları temizleyelim.

Senaryomuz basit. Bir kitap bilgilerini içeren uygulamamız ve bu kitapları türlerini tutacağımız bir enum değerimiz olsun. Uygulama üzerinden CRUD işlemlerini gerçekleştirelim.

Bunun için Domain -> Entities, Domain -> Enum isminde klasörler oluşturarak nesne tanımlarımızı oluşturalım.

Üç adet yeni nesne tanımımız mevcut.

Kitap bilgilerimizi tutacağımız Book.cs

using CqrsMediatorPattern.Domain.Enums;

namespace CqrsMediatorPattern.Domain.Entities;

public class Book
{
public long Id { get; set; }
public string Title { get; set; }
public string Description { get; set; }    
public BookGenre BookGenre { get; set; }
public DateTime Created { get; set; }
public DateTime Updated { get; set; }
}

Kitap türlerini tutacağımız enum tipinde olan BookGenre.cs

namespace CqrsMediatorPattern.Domain.Enums;

public enum BookGenre
{
    None = 0,
    Horror = 1,
    Fable = 2,
    Fiction = 3,
    Mystery = 4
}

Veri tabanı işlemlerimiz için projemize Entity Framework Core paketlerini eklememiz gerekmekte. Bunun için aşağıdaki paketleri paket yöneticisi (Nuget Packet Manager) , CLI veya paket referans (PacketReference) olarak yükleyebilirsiniz.

Microsoft.EntityFrameworkCore.SqlServer
Microsoft.EntityFrameworkCore.Tools
Microsoft.EntityFrameworkCore
Nuget paket yöneticisi üzerinden paketlerin eklemesi

Şimdi de Dbcontext sınıfımızı tasarlayabiliriz. Data -> Context isimli bir klasör açarak Dbcontext sınıfımız orada tasarlıyoruz. Sınıfımızın ismi BookDbContext olacak şekilde bir düzenleme yapıyoruz.

BookDbContext.cs
using CqrsMediatorPattern.Domain.Entities;
using Microsoft.EntityFrameworkCore;

namespace CqrsMediatorPattern.Data.Context;

public class BookDbContext : DbContext
{
public BookDbContext(DbContextOptions<BookDbContext> options) : base(options)
    {

    }

public DbSet<Book> Books { get; set; }    
}

appsettings.json dosyamızda ConnectionString bilgilerini güncellememiz gerekiyor. Bilgileri aşağıdaki gibi ekliyoruz.

{kullanici_adiniz} : Sql Server için belirlenen kullanıcı adı
{şifreniz} : Sql Server için belirlenen şifreniz

{
"ConnectionString": {
"SqlDbConnectionString": "Data Source=.\\SQLEXPRESS; Initial Catalog=CqrsMediatr_example;Persist Security Info=True;User Id={kullanici_adiniz};Password={şifreniz};MultipleActiveResultSets=true;TrustServerCertificate=True"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}

Ardından Program.cs sınıfımızda oluşturmuş olduğumuz DbContext nesnesini register etmemiz gerekiyor. Bunun için Program.cs sınıfında aşağıdaki gibi eklemeyi yapıyoruz.

using CqrsMediatorPattern.Data.Context;
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.

builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

builder.Services.AddDbContextPool<BookDbContext>(
    options => options.UseSqlServer(builder.Configuration["ConnectionString:SqlDbConnectionString"]));

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseAuthorization();

app.MapControllers();

app.Run();

Dah asonra veri tabanımızı ve tabloları oluşturmak için aşağıdaki komutları sırasıyla Package Manager Console üzerinde çalıştırıyoruz.

Add-Migration Init
Update-Database
Add-Migration
Update-Database

Ardından Sql Server üzerinden kontrol ettiğimizde veri tabanı ve tabloların oluşmuş olduğu görebiliriz.

Şimdi CQRS için geliştirmelerimizi yapabiliriz. İlk olarak CQRS prensibini hayata geçireceğiz. İyi ve kötü yanlarını kod olarak gördükten sonra Mediator prensibinin sağladığı yararları ve MediatR kütüphanesinin kullanımına bakacağız.

Öncelikle CQRS isminde bir klasör açarak Commands, Handlers ve Queries bilgilerimizi burada oluşturuyoruz.

CQRS klasörü ve içeriği

Commands : Uygulama içinde yapılacak olan tüm komut (CUD) işlemlerinin tanımlandığı klasör olacaktır.

Queries : Uygulama içinde yapılacak olan tüm sorgu (R) işlemlerinin tanımlandığı klasör olacaktır.

Handler : Tüm komut ve sorguları ortak noktada işleyecek ve kullanıcılara uygun olacak cevabı dönecek olan modellerimizin tanımlandığı klasör olacaktır.

Şimdi Commands klasörü içindeki nesnelerimizi tasarlayalım.

CreateBookCommandRequest.cs ve CreateBookCommandResponse.cs

using CqrsMediatorPattern.Domain.Entities;
using CqrsMediatorPattern.Domain.Enums;

namespace CqrsMediatorPattern.CQRS.Commands.Request;

public class CreateBookCommandRequest
{
public string? Title { get; set; }
public string? Description { get; set; }    
public BookGenre? BookGenre { get; set; }
}
namespace CqrsMediatorPattern.CQRS.Commands.Response;

public class CreateBookCommandResponse
{
public bool IsSuccess { get; set; }
public int CreatedBookId { get; set; }
}

DeleteBookCommandRequest.cs ve DeleteBookCommandResponse.cs

namespace CqrsMediatorPattern.CQRS.Commands.Request;

public class DeleteBookCommandRequest
{
public int BookId { get; set; }
}
namespace CqrsMediatorPattern.CQRS.Commands.Response;

public class DeleteBookCommandResponse
{
public bool IsSuccess { get; set; }
}
Command nesnelerimiz eklendikten sonra son durum

Şimdi Queries klasörü içindeki nesnelerimizi tasarlayalım.

GetAllBooksQueryRequest.cs ve GetAllBookQueryResponse.cs

namespace CqrsMediatorPattern.CQRS.Queries.Request;

public class GetAllBooksQueryRequest
{

}
using CqrsMediatorPattern.Domain.Enums;

namespace CqrsMediatorPattern.CQRS.Queries.Response;

public class GetAllBooksQueryResponse
{
public long Id { get; set; }
public string? Title { get; set; }
public string? Description { get; set; }
public BookGenre BookGenre { get; set; }
public DateTime Created { get; set; }
public DateTime Updated { get; set; }
}

Şimdi de sıra Handlers klasörüne geldi. Burada komut (Command) ve sorgu (Query) isteklerini işleyeceğimiz yapıları geliştireceğiz.

CommandHandlers

CreateBookCommandHandler.cs

using CqrsMediatorPattern.CQRS.Commands.Request;
using CqrsMediatorPattern.CQRS.Commands.Response;
using CqrsMediatorPattern.Data.Context;
using CqrsMediatorPattern.Domain.Entities;

namespace CqrsMediatorPattern.CQRS.Handlers.CommandHandlers;

public class CreateBookCommandHandler
{
private readonly BookDbContext _dbContext;

public CreateBookCommandHandler(BookDbContext dbContext)
    {
this._dbContext = dbContext;
    }

public CreateBookCommandResponse CreateBook(CreateBookCommandRequest request)
    {
        _ = _dbContext.Books.Add(new Book
        {
            BookGenre = request.BookGenre.Value,
            Created = DateTime.Now,
            Description = request.Description,
            Title = request.Title
        });

var idInformation = _dbContext.SaveChanges();

return new CreateBookCommandResponse { IsSuccess= true, CreatedBookId = idInformation };
    }
}

DeleteBookCommandHandler.cs

using CqrsMediatorPattern.CQRS.Commands.Request;
using CqrsMediatorPattern.CQRS.Commands.Response;
using CqrsMediatorPattern.Data.Context;

namespace CqrsMediatorPattern.CQRS.Handlers.CommandHandlers;

public class DeleteBookCommandHandler
{
private readonly BookDbContext _dbContext;

public DeleteBookCommandHandler(BookDbContext dbContext)
    {
this._dbContext = dbContext;
    }

public DeleteBookCommandResponse DeleteBook(DeleteBookCommandRequest request)
    {
var findBookResult = _dbContext.Books.FirstOrDefault(p => p.Id == request.BookId);

if (findBookResult == null)
        {
return new DeleteBookCommandResponse { IsSuccess = false };
        }

        _dbContext.Books.Remove(findBookResult);

        _ = _dbContext.SaveChanges();

return new DeleteBookCommandResponse { IsSuccess = true };
    }
}

QueryHandlers

GetAllBooksQueryHandler.cs

using CqrsMediatorPattern.CQRS.Queries.Request;
using CqrsMediatorPattern.CQRS.Queries.Response;
using CqrsMediatorPattern.Data.Context;

namespace CqrsMediatorPattern.CQRS.Handlers.QueryHandlers;

public class GetAllBooksQueryHandler
{
private readonly BookDbContext _dbContext;

public GetAllBooksQueryHandler(BookDbContext dbContext)
    {
this._dbContext = dbContext;
    }

public List<GetAllBooksQueryResponse> GetAllBooks(GetAllBooksQueryRequest request)
    {
return _dbContext.Books.Select(x => new GetAllBooksQueryResponse()
        {
            BookGenre = x.BookGenre,
            Created = x.Created,
            Description = x.Description,
            Id = x.Id,
            Title = x.Title,
            Updated = x.Updated
        }).ToList();
    }
}

Temel olarak CQRS prensibi ile sınıflarımızı geliştirmiş olduk. Şimdi Handler olarak geliştirdiğimiz sınıfları servis olarak uygulamamıza eklememiz ve ardından Controller sınıfımızı geliştirmemiz gerekiyor. Program.cs sınıfımızın son hali aşağıdaki gibi olacaktır.

using CqrsMediatorPattern.CQRS.Handlers.CommandHandlers;
using CqrsMediatorPattern.CQRS.Handlers.QueryHandlers;
using CqrsMediatorPattern.Data.Context;
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.

builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

builder.Services.AddDbContextPool<BookDbContext>(
    options => options.UseSqlServer(builder.Configuration["ConnectionString:SqlDbConnectionString"]));

builder.Services.AddTransient<CreateBookCommandHandler>();
builder.Services.AddTransient<DeleteBookCommandHandler>();
builder.Services.AddTransient<GetAllBooksQueryHandler>();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseAuthorization();

app.MapControllers();

app.Run();

Şimdi de Controller sınıfımızı geliştirelim. Controllers klasörümüzün içine BookController isminde bir API Controller oluşturuyoruz.

BookController.cs sınıfımızın son hali de aşağıdaki gibi olacaktır.

using CqrsMediatorPattern.CQRS.Commands.Request;
using CqrsMediatorPattern.CQRS.Commands.Response;
using CqrsMediatorPattern.CQRS.Handlers.CommandHandlers;
using CqrsMediatorPattern.CQRS.Handlers.QueryHandlers;
using CqrsMediatorPattern.CQRS.Queries.Request;
using Microsoft.AspNetCore.Mvc;

namespace CqrsMediatorPattern.Controllers;

[Route("api/[controller]")]
[ApiController]
public class BookController : ControllerBase
{
readonly CreateBookCommandHandler createBookCommandHandler;
readonly DeleteBookCommandHandler deleteBookCommandHandler;
readonly GetAllBooksQueryHandler getAllBooksQueryHandler;

public BookController(CreateBookCommandHandler createBookCommandHandler, DeleteBookCommandHandler deleteBookCommandHandler, GetAllBooksQueryHandler getAllBooksQueryHandler)
    {
this.createBookCommandHandler = createBookCommandHandler;
this.deleteBookCommandHandler = deleteBookCommandHandler;
this.getAllBooksQueryHandler = getAllBooksQueryHandler;
    }

    [HttpGet]
public IActionResult Get([FromQuery] GetAllBooksQueryRequest request)
    {
var allBooks = getAllBooksQueryHandler.GetAllBooks(request);

return Ok(allBooks);
    }        

    [HttpPost]
public IActionResult Post([FromBody] CreateBookCommandRequest request)
    {
        CreateBookCommandResponse response = createBookCommandHandler.CreateBook(request);

return Ok(response);
    }

    [HttpDelete("{id}")]
public IActionResult Delete([FromQuery] DeleteBookCommandRequest request)
    {
        DeleteBookCommandResponse response = deleteBookCommandHandler.DeleteBook(request);
return Ok(response);
    }
}

Ardından projemizi çalıştırıp Swagger üzerinden test edebiliriz.

İlk önce bir kitap ekleyerek ardından liste olarak kitapları alacağız.

Yeni Ktap Ekleme
Yeni Kitap Ekleme Sonucu
Kitap Listesi
Kitap Listesi Sonucu
Veri tabanında bulunan kayıt bilgisi

Gördüğünüz gibi BookController üzerinden isteklerimizi göndererek Command ve Query nesnelerimizi ayırıp işlemlerimizi farklı akışlarla yapabildik.

Biz tek bir nesne ve tablo için bu işlemleri yerine getirdik. Fakat proje büyüdükçe Command ve Query nesne sayılarımız artacak. Servis olarak uygulamamıza eklediğimiz sınıfılar fazlalaşacak ve özellik Controller sınıfıların DI işlemleri problem olmaya başlayacak gibi görünüyor.

Bir kötü durum da gelen istek (Request) bilgisinin cevap (Response) bilgisini bizim belirlememiz. Gelen isteğe göre uygun olan Handler bilgisinin de yazılımcı tarafında belirlenmesi gibi işler de projenin geliştirme aşamalarında bizlere zorluk çıkaracak, karmaşıklığı artıracaktır.

Böyle bir durumda da Mediator prensibi bize destek olacak ve MediatR kütüphanesi ile bu prensibi ekleyerek daha kolay ve daha rahat bir şekilde CQRS nesnelerimizi kullanabileceğiz. Böylelik Command, Query ve Handler nesnelerimizin davranışlarına uygun şekilde çalışmalarını ve yönetilmelerini sağlayabileceğiz.

Gelin beraber MediatR geliştirmelerini hazırlamaya başlayalım.

İlk önce MediatR kütüphanesini projemize ekleyerek başlıyoruz.

MediatR kütüphanesini projemize eklemek için aşağıdaki paketleri paket yöneticisi (Nuget Packet Manager) , CLI veya paket referans (PacketReference) olarak yükleyebilirsiniz.

MediatR
MediatR.Extensions.Microsoft.DependencyInjection
Nuger paket manager ile MediatR yüklenmesi

Ardından Program.cs sınıfımıza servis kayıt işlemini gerçekleştiriyoruz.

using CqrsMediatorPattern.CQRS.Handlers.CommandHandlers;
using CqrsMediatorPattern.CQRS.Handlers.QueryHandlers;
using CqrsMediatorPattern.Data.Context;
using MediatR;
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.

builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

builder.Services.AddDbContextPool<BookDbContext>(
    options => options.UseSqlServer(builder.Configuration["ConnectionString:SqlDbConnectionString"]));

builder.Services.AddTransient<CreateBookCommandHandler>();
builder.Services.AddTransient<DeleteBookCommandHandler>();
builder.Services.AddTransient<GetAllBooksQueryHandler>();

builder.Services.AddMediatR(typeof(Program));

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseAuthorization();

app.MapControllers();

app.Run();

MediatR kütüphanesi bize iki adet ara yüz sunmaktadır. Bu ara yüzler sayesinde Request ve RequestHanler nesnelerimizi belirleyerek Mediator prensibine uygun olarak sorumluluklarının almalarını sağlayabilmektedir.

Bu ara yüzler aşağıdaki gibidir.

IRequest :

namespace MediatR
{
//
// Summary:
//     Marker interface to represent a request with a void response
public interface IRequest : IRequest<Unit>, IBaseRequest
    {
    }
}

IRequestHandler :

namespace MediatR
{
//
// Summary:
//     Defines a handler for a request
//
// Type parameters:
//   TRequest:
//     The type of request being handled
//
//   TResponse:
//     The type of response from the handler
public interface IRequestHandler<in TRequest, TResponse> where TRequest : IRequest<TResponse>
    {
//
// Summary:
//     Handles a request
//
// Parameters:
//   request:
//     The request
//
//   cancellationToken:
//     Cancellation token
//
// Returns:
//     Response from the request
Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken);
    }
}

Şimdi de daha önce oluşturduğumuz tüm Command, Query ve Handler sınıflarımızı MediatR için yenidne düzenleyelim. Burada yapacağımız CommandRequest nesnelerinin IRequest, Handler nesnelerimizin ise IRequestHandler ara yüzlerini uyarlamalarının sağlamak olacaktır.

CreateBookCommandRequest.cs

using CqrsMediatorPattern.CQRS.Commands.Response;
using CqrsMediatorPattern.Domain.Enums;
using MediatR;

namespace CqrsMediatorPattern.CQRS.Commands.Request;

public class CreateBookCommandRequest: IRequest<CreateBookCommandResponse>
{
public string? Title { get; set; }
public string? Description { get; set; }
public BookGenre? BookGenre { get; set; }
}

DeleteBookCommandRequest.cs

using CqrsMediatorPattern.CQRS.Commands.Response;
using MediatR;

namespace CqrsMediatorPattern.CQRS.Commands.Request;

public class DeleteBookCommandRequest: IRequest<DeleteBookCommandResponse>
{
public int BookId { get; set; }
}

GetAllBooksQueryRequest.cs

using CqrsMediatorPattern.CQRS.Queries.Response;
using MediatR;

namespace CqrsMediatorPattern.CQRS.Queries.Request;

public class GetAllBooksQueryRequest:IRequest<List<GetAllBooksQueryResponse>>
{

}

Şimdi de Handler nesnelerimizin değişikliklerini yapalım. Handler nesneleri için IRequestHandler<request,response> şeklinde bir ara yüz uyarlaması yapacağız. Ardından ara yüz ile birlikte gleen Handler metodunu ekleyeceğiz. Burada fark yeni eklenen Handle metodunun async olarak çalışacak olmasıdır.

CreateBookCommandHandler.cs

using CqrsMediatorPattern.CQRS.Commands.Request;
using CqrsMediatorPattern.CQRS.Commands.Response;
using CqrsMediatorPattern.Data.Context;
using CqrsMediatorPattern.Domain.Entities;
using MediatR;

namespace CqrsMediatorPattern.CQRS.Handlers.CommandHandlers;

public class CreateBookCommandHandler: IRequestHandler<CreateBookCommandRequest, CreateBookCommandResponse>
{
private readonly BookDbContext _dbContext;

public CreateBookCommandHandler(BookDbContext dbContext)
    {
this._dbContext = dbContext;
    }    

public Task<CreateBookCommandResponse> Handle(CreateBookCommandRequest request, CancellationToken cancellationToken)
    {
        _ = _dbContext.Books.Add(new Book
        {
            BookGenre = request.BookGenre.Value,
            Created = DateTime.Now,
            Description = request.Description,
            Title = request.Title
        });

var idInformation = _dbContext.SaveChanges();

return Task.FromResult(new CreateBookCommandResponse { IsSuccess = true, CreatedBookId = idInformation });
    }
}

DeleteBookCommandHandler.c

using CqrsMediatorPattern.CQRS.Commands.Request;
using CqrsMediatorPattern.CQRS.Commands.Response;
using CqrsMediatorPattern.Data.Context;
using MediatR;

namespace CqrsMediatorPattern.CQRS.Handlers.CommandHandlers;

public class DeleteBookCommandHandler : IRequestHandler<DeleteBookCommandRequest, DeleteBookCommandResponse>
{
private readonly BookDbContext _dbContext;

public DeleteBookCommandHandler(BookDbContext dbContext)
    {
this._dbContext = dbContext;
    }

public Task<DeleteBookCommandResponse> Handle(DeleteBookCommandRequest request, CancellationToken cancellationToken)
    {
var findBookResult = _dbContext.Books.FirstOrDefault(p => p.Id == request.BookId);

if (findBookResult == null)
        {
return Task.FromResult(new DeleteBookCommandResponse { IsSuccess = false });
        }

        _dbContext.Books.Remove(findBookResult);

        _ = _dbContext.SaveChanges();

return Task.FromResult(new DeleteBookCommandResponse { IsSuccess = true });
    }
}

GetAllBooksQueryHandler.cs

using CqrsMediatorPattern.CQRS.Queries.Request;
using CqrsMediatorPattern.CQRS.Queries.Response;
using CqrsMediatorPattern.Data.Context;
using MediatR;

namespace CqrsMediatorPattern.CQRS.Handlers.QueryHandlers;

public class GetAllBooksQueryHandler: IRequestHandler<GetAllBooksQueryRequest, List<GetAllBooksQueryResponse>>
{
private readonly BookDbContext _dbContext;

public GetAllBooksQueryHandler(BookDbContext dbContext)
{
this._dbContext = dbContext;
    }    

public Task<List<GetAllBooksQueryResponse>> Handle(GetAllBooksQueryRequest request, CancellationToken cancellationToken)
    {
return Task.FromResult(_dbContext.Books.Select(x => new GetAllBooksQueryResponse()
        {
            BookGenre = x.BookGenre,
            Created = x.Created,
            Description = x.Description,
            Id = x.Id,
            Title = x.Title,
            Updated = x.Updated
        }).ToList());
    }
}

Şimdi ise BookController sınıfımızı da MediatR’a uygun hale getiriyoruz. Burada yapacağımız değişiklik MediatR’ın bizim için snıflarımızı yönlendirmesi adın Controller sınıfına kendisini inject ederek yönlendirme işlemini ona bırakacağız.

BookController.cs

using CqrsMediatorPattern.CQRS.Commands.Request;
using CqrsMediatorPattern.CQRS.Commands.Response;
using CqrsMediatorPattern.CQRS.Queries.Request;
using MediatR;
using Microsoft.AspNetCore.Mvc;

namespace CqrsMediatorPattern.Controllers;

[Route("api/[controller]")]
[ApiController]
public class BookController : ControllerBase
{
readonly IMediator mediator;

public BookController(IMediator mediator)
    {
this.mediator = mediator;
    }

    [HttpGet]
public async Task<IActionResult> Get([FromQuery] GetAllBooksQueryRequest request)
    {
var allBooks = await mediator.Send(request);

return Ok(allBooks);
    }        

    [HttpPost]
public async Task<IActionResult> Post([FromBody] CreateBookCommandRequest request)
    {
        CreateBookCommandResponse response = await mediator.Send(request);

return Ok(response);
    }

    [HttpDelete("{id}")]
public async Task<IActionResult> Delete([FromQuery] DeleteBookCommandRequest request)
    {
        DeleteBookCommandResponse response = await mediator.Send(request);

return Ok(response);
    }
}

Görüldüğü gibi daha önceden eklemiş olduğumuz Handler sınıflarımızın injection bilgilerini kaldırdık. Controller metotlarımızı async olacak şekilde yeniden düzenledik ve tüm işi mediator ismi ile eklediğimiz nesneye bıraktık.

Son olarak daha önce Program.cs sınıfımızında kayıt ettiğimiz Handler servislerimizi de kaldırabiliriz.

Program.cs

using CqrsMediatorPattern.CQRS.Handlers.CommandHandlers;
using CqrsMediatorPattern.CQRS.Handlers.QueryHandlers;
using CqrsMediatorPattern.Data.Context;
using MediatR;
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.

builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

builder.Services.AddDbContextPool<BookDbContext>(
    options => options.UseSqlServer(builder.Configuration["ConnectionString:SqlDbConnectionString"]));

builder.Services.AddMediatR(typeof(Program));

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseAuthorization();

app.MapControllers();

app.Run();

Uygulamamızı çalıştırıp kitapları listelediğimiz zaman sorunsuz bir şekilde kitapların geldiğini göreceğiz.

Bu yazıda CQRS prensibinin büyük ölçekli uygulamaları oluştururken bize performans, yönetim ve geliştirme noktalarında sağladığı kolaylıkları inceledik. İyi yanlarının yanında kötü yanları ile de gelen CQRS için MediatR kütüphanesi ile nasıl kolay çözümler üretebileceğimize Mediator prensibini de inceleyerek incelemeye çalıştık.

CQRS yapısı istenildiği zaman tek başına istenildiğinde ise Meditor ile birlikte kullanılabilemekte ve bizlere geliştirme konusunda kolaylıklar sağlamaktadır.

Başka bir incelemede görüşmek üzere…

Projenin Github adresine aşağıdaki adresten ulaşabilirsiniz.

https://github.com/onurkarakus/CqrsMediatorPattern

Nedir bu Mikro Servisler (Microservices) ?

Merhaba,

Son dönem çok defa ismini duyduğumuz, neredeyse herkesin ya yeni uygulama geliştirmeye başlarken ya da mevcut uygulamalarını bu yapıya güncellemek için düşüncelere daldığı mikro servis konusunu inceleyeceğiz. Öncelikle mevcut durumun ardından mikro servis konusunu inceledikten sonra API Gateway modeli üzerinden bir örneklendirme yapacağız.

O zaman ? Buyursunlar.

Mikro Servis Yapısı Hakkında

Monolitik Mimari Yapılar
Mikro servislerden bahsetmeden önce biraz mikro servis öncesi durumları incelemek yararlı olacak diye düşünüyorum. Nereden ? Nereye ? Büyük ihtimalle şimdiye kadar hatta şu an bile geliştirdiğimiz uygulamalar monolit yapıdaki uygulamalar. (Benim öyle şu an) Peki nedir bu monolitik yapı?

Monolitik mimari geniş bir kitle tarafından kullanılan geleneksel bir mimari aslında. Geliştirdiğimiz uygulamanın tüm parçalarının aynı proje içinde olması diyebiliriz. Peki, biz zaten gevşek bağlanmış (loosely coupled) uygulamalar geliştiriyoruz. Monolitik mimari burada neyi değiştiriyor diyebilirsiniz. Açıklamaya çalışayım efendim.

Uygulama içerisinde kullandığımız tüm bileşenlerimiz birbirleri ile gevşek bağlı olabilir fakat uygulamamızın içerisinde bulunan her şey uygulamanın kendisi ile sıkı sıkıya bağlı durumda. Bunun en güzel örneği dağıtım (deployment) aşamasında karşımıza çıkıyor. Eğer biz uygulamamız içerisinde bir güncelleme yaparsak ve bunu üretim (production) ortamında yayınlamak istersek o zaman tüm uygulamayı baştan yayınlama aşamasına göndermemiz gerekiyor. Aynı durum üretim ortamında çalışan uygulamalarımızda oluşan sorunlar karşısında da ortaya çıkıyor.

Örnek vermek gerekirse bir bankacılık uygulaması içerisinde kullanıcıların varlıkları ile ilgili işlemler yapılıyor. Hesap bilgileri, kredi kartı bilgileri, yatırım bilgileri vb. Eğer kart işlemleri modülü ile ilgili bir hata oluşursa uygulamamız kartların bilgileri çekerken hata alacak durumda.

“Ne var? O noktada hata alırsa bir yolla hatayı fırlatmaz log olarak sisteme yazabilirim. Veya yumuşak bir geçiş ile kullanıcının mutluluğunu bozmam.” Evet olası bir durum, yapılabilir fakat ne kadar doğru? Tabii uygulamamız için yapacağımız yeni geliştirmelerde de bu sorun mevcut. Kart ekstreleri ile ilgili bir geliştirme yaptınız. Bunun üretim ortamına çıkması gerekiyor. O zaman tüm uygulamanın üretim ortamına alınması, hatta farklı yerlerin etkilenip / etkilenmediği kontrolleri vs. vs. Sadece kart modülü için yapılan bir değişiklik için sizi uzun bir yolculuk bekliyor aslında.

Mikro Servis Mimarisi
Bu mimaride uygulamamızı her birinin farklı bir hizmet verdiği bileşenlere ayırıyoruz. Her bileşen ayrı bir uygulama olarak yaşam döngüsünü sürdürüyor. Bankacılık uygulaması örneğimize geri dönecek olursak hesaplar, kartlar ve yatırım ürünlerinin ayrı birer küçük uygulama olduklarını düşünelim. Bu küçük uygulamalara bölmenin bize kattıkları konusunu inceleyelim.

Hesap bilgilerini dönen servisimiz gayet güzel çalışırken, kartları dönen servisimizde bir hata olması durumunda sistemin geneli etkilenmeden akışlarımız devam edebilir.

Bu küçük uygulamalardan herhangi birinde bir yük artışı olduğu zaman (küçük olmalarından dolayı) hızlı bir şekilde yatay olarak ölçeklendirilebilir. (horizontal scale). Daha sonrasında eğer yük azalırsa ölçeklendirme sonucunda oluşan yeni makinelerin kapatılması da söz konusu. (Docker ve Kubernetes)

Yine, bu küçük uygulamalardan herhangi birinde yapacağımız bir kod güncellemesini diğer uygulamalar etkilenmeden üretim ortamına alabilme yeteneğimiz de oluyor.

O zaman genel olarak baktığımız zaman mikro servis mimarisi, uygulamanın kendisinin çeşitli bileşenlere bölündüğü ve her bileşenin belirli bir amaca hizmet ettiği bir mimaridir. Bileşenler artık uygulamanın kendisine bağımlı değildir. Bu bileşenlerin her biri tam anlamıyla ve fiziksel olarak bağımsızdır. Bu şekilde kurulmuş olan bir mimari ile uygulamalarımız ayrı ayrı veri tabanlarına sahip olabildikleri gibi ayrı makinelerde de barındırılabilirler.

Avantajları

  • Mikro servislerin her biri ayrı uygulama olarak çalıştıklarından dolayı ayrı programlama dili ile de yazılabilir.
  • Ekibe yeni katılan bir geliştiricinin (developer) tüm yapıya hakim olması için geçecek olan zamanı azaltarak belirli mikro servisler üzerinde hızlı bir şekilde çalışmaya başlaması sağlanabilir.
  • Dağıtım (deployment) aşaması her mikro servis için ayrı ayrı yapılabilir. Bu da deploy sürelerini kısaltmış olur.
  • Mikro servislerin çalıştıkları konteynır (container) üzerinde yüke göre ölçeklendirme uygulanabilir.
  • İstenildiği zaman farklı veri tabanlarında çalışacak mikro servisler geliştirilecek yapıya dahil edilebilir. Örnek olarak MongoDb ile çalışacak ve Go ile yazılmış bir mikro servisi de yapınıza sonradan dahil edebilir ve ortak çalışma alanında bu mikro servis üzerinden faydalanabilirsiniz.

Dezavantajları

  • Transaction bütünlüğünün sağlanmasındaki zorluklar.
  • Servisler arası iletişim ve bütünlük zorlayıcıdır.
  • Integration testlerin hazırlanması zordur.
  • Genel olarak iş mantığına iyi derecede hakim olmamız gerekmektedir.
  • Dağıtım (deployment) sonrası kontrol, güvenlik ve yönetim işlemlerinin iyi bir şekilde yapılması gerekmektedir. Servis sayımız çoğaldıkça bu konularda da zorluklar ortaya çıkmaktadır.

Şimdi de ufak bir mikro servis örneği ile devam edelim. Bu çalışmada bir yazar ve bir kitap servisimiz olacak. Bu servislerimiz bize sadece kendi konuları ile ilgili bilgileri verecekler.

Burada bir soru işareti ortaya çıkıyor. Bu servisleri çağıracak olan client hepsinin adresini bilmesi mi gerekiyor? Hayır, buna gerek yok. Biz bir API Gateway ekleyerek client uygulamanın bu adrese gelmesini sağlayacağız. Client API gateway üzerinden hangi işlemi isterse gateway bizim için yönlendirme (routing) işlemini yapacak ve uygun servis cevaplarını dönecek. Bunun için Ocelot kütüphanesini kullanacağız.

İlk olarak yeni bir boş çözüm (blank solution) oluşturarak ismin MyMicroServices vererek projemize başlıyoruz.

Oluşturacağımız mikro servisleri ayırabilmek için MicroServices isimli bir klasör oluşturuyoruz.

Şimdi oluşturduğumuz klasörümüze iki adet ASP.NET Core Web API projesi oluşturacağız. İsimlerini Author.Microservice ve Book.Microservice olarak vereceğiz.

Son olarak da API Gateway olarak görevlendireceğimiz MyGateWay.WebApi isimli projemizi oluşturacağız. Bu projemizi ASP.NET Core Empty proje temasını (template) kullanarak oluşturuyoruz.

Son durumda projemin yapısı aşağıdaki gibi olacaktır.

Author.Microservice isimli projeme Entities isminde bir klasör ekleyerek AuthorInformation.cs isimli sınıfımı (class) ekliyorum.

Ardından Controllers klasörüne AuthorController isminde API Controller sınıfımızı ekleyerek bize yazar listesi döndürecek olan metodumuzu ekliyoruz.

Postman üzerinden API metodumuzu test ettiğimiz zaman aşağıdaki şekilde bilgileri döndürdüğünü görebiliriz.

Aynı şekilde Book.Microservice uygulaması içinde bir Entities klasörü açarak BookInformation.cs sınıfını ekleyeceğiz. Ardından Controllers klasörüne BookController.cs sınıfımızı ekleyerek kitap bilgilerini dönecek metodu yazacağız.

Ocelot API Gateway hakkında biraz daha
Ocelot open source olarak dağıtılan .NET/Core platformları için geliştirilmiş bir API Gateway ürünüdür. Arkasında bulunan mikro servislerin adreslerini bir konfigürasyon dosyasına kayıt ederek gelen isteğin hangi mikro servise gitmesi gerektiğini tanımlayacağız. Böylelikle client bir istekte bulunduğu cama Ocelot bizim için yönlendirmeyi yapacak ve uygun cevabı dönecek.

Konfigürasyon dosyası json formatındadır. Burada rotalarımızı (route) tanımlarken karşımıza iki terim çıkıyor. Bunlar; UpStream ve DownStream UpStream client’ın geleceği adresi tarif ederken DownStream ise Ocelot’un çağıracağı iç servis bilgisini tanımlamaktadır.

Ocelot API Gateway olarak servislerin rotalarının oluşturulması haricinde kimlik doğrulama (Authentication), yetki (Authorization) ve yük dengeleme (Load Balance) işlemlerini de bizim için yapabilmektedir.

MyGateWay.WebApi projesine Ocelot paketini ekliyoruz. Bunun için isterseniz Install-Package Ocelot komutunu isterseniz de Nuget Packet Manager üzerinden paketin yüklemesini yapabilirsiniz.

Daha sonrasında Program.cs sınıfımız içerisinde aşağıdaki değişikliği yapıyoruz.

Buradaki amacımız uygulamamızın açılışında ocelot.json isimli dosya içerisinden Ocelot konfigürasyon bilgilerinin alınması.

Şimdi de Startup.cs isimlii sınıfın içerisinde bulunan ConfigureServices metoduna Ocelot’u ekleyelim. Ardından yine aynı sınıf içinde bulunan Configure metodunda da Ocelot kullanımı için tanım yapacağız.

Şimdi de sıra Ocelot üzerinde rota bilgilerini tanımlama kısmına geldi. Öncelikle burada iki adet mikro servisin hangi portlar üzerinden çalıştığının kontrol etmemiz. Daha sonrasında ise VS üzerinden uygulamalardan birini çalıştırdığımız zaman hepsinin ayağa kalkması için bir değişiklik yapmamız gerekecek.

Önce servis portları. Bunun için Author.Microservice ve Book.Microservice projelerine tek tek sağ tuşal tıklayarak açılan Properties penceresinde sol bölümde bulunan Debug menüsünü açıyoruz. Port bilgisi sayfa üzerinde gelecektir.

Burada port bilgisini Book.Microservice için 1360 Author.Microservice için 1361 olarak değiştiriyoruz. Bu değişikliği isterseniz projelerin her birinde bulunan launchSettings.json dosyası üzerinden de yapmanız mümkün.

Uygulamaların hepsinin aynı anda çalıştırılması için Solution Explorer üzerinden Solution dosyasına sağ tuşla tıklayarak Properties menüsü üzerinden açılan sayfada değişiklikleri aşağıdaki gibi yapabilirsiniz.

Şimdi Ocelot üzerinde rota tanımlarımızı yapabiliriz. Bunun için daha önceden tanımını yaptığımız ocelot.json isimli dosyayı MyGateway.WebApi projemizin root’una tanımlıyoruz. Ardından aşağıdaki şekilde rota (route) tanımlarının yapıyoruz.

Burada yaptığımız tanımlamaları biraz açıklamak gerekiyor.

Routes altında tanımladığımız;

DownstreamPathTemplate: Ocelot’un gideceği API rotası olarak açıklanabilir. 
DownstreamScheme: http olarak veya https olarak gidileceğinin tanımı. 
DownstreamHostAndPorts: Author servisi için kullanılacak olan Ocelot arkasındaki adres bilgisi.

UpstreamPathTemplate: Client uygulamanın gateway’e geleceği API rota bilgisi. 
UpstreamHttpMethod: Client uygulamanın hangi HTTP Verb için bu adrese geleceğini tanımladığımız yerdir.

Ardından tanımlarımızı ve servislerimizi test edebiliriz. Daha önceden Project Properties penceresinden tüm uygulamalar beraber çalışmaya başlasın dediğimiz için F5 dememiz servislerin hepsinin çalışması için yeterli olacaktır.

Eğer pencere üzerinden bir değişiklik yapmadıysanız hepsini tek tek çalıştırmanız gerekmektedir.

Postman ile API Gateway uygulamamıza hem kitaplar hem de yazarlar için çağrılarımızı yapalım. Bakalım nasıl bir bilgi gelecek.

Gördüğünüz gibi hem http://localhost:5000/gateway/Book hem de http://localhost:5000/gateway/Author adresleri API Gateway uygulamamız içerinden çağırılan adresler. Bu adresleri Postman uygulaması ile çağırdığımız zaman Ocelot bizim için uygun olan servise gidiyor ve o servis sonucu alarak bize geri iletiyor.

Böylelikle client’ın hangi servisi nasıl çağırması gerektiğini bilmesine gerek kalmıyor. Client sadece API Gateway adresini biliyor ve oraya geliyor.

Umarım mikro servis mimarileri ve temel çalışma prensiplerini anlama konusunda yararlı bir makale olmuştur.

Görüşmek üzere…

Kaynaklar:

http://mustafabas.me/tr/mikroservis-mimarisi-nedir-avantajlari-dezavantajlari-nelerdir–b-1006

Microservice Architecture in ASP.NET Core with API Gateway

RabbitMQ İle İlk Adımlar

İmaj adresi : https://www.rabbitmq.com/img/home/banner/webinar/RabbitMQ-Hero-queues-desktop.svg

RabbitMQ’ya bağlanma, kanal ve kuyruk oluşturma, mesajlarımızı gönderip, okuma işlemlerini incelemeye çalıştım. Umarım anlaşılır olmuştur.

Projenin Github bağlantısı aşağıdaki gibidir.
https://github.com/onurkarakus/RabbitMQMessageApplication

Merhaba,

Çocukluğundan kalan bir anı sanırım. Büyüdüğüm zamanlarda artık çok gitmediğim şehrimizde olan fuarlar hep aklımda. Kalabalık olmasının yanı sıra farklı türde ve tarzda bir sürü eğlence yerinin olmasından dolayı seslerin siz gezerken kulağınızda zorlayıcı etkileri. Bir yandan çalan rock müziğin yanında diğer yandan gelen türkü seslerinin karışması. Tabii bunlara eklenen insanların konuşmaları, birbirlerine seslenmeleri ve çocukların neşeli sesleri. Aslında tam bir kaos ortamı. fuar alanından çıktıktan sonra aklınızda kalan tek şey karışıklığın verdiği yorgunluk oluyordu.

Uygulamalarımızda aslında bu şekilde bir yapıya sahip diye düşünüyorum. Bir sürü mesajın düzensiz bir şekilde gönderip alınması ile birlikte ciddi performans sorunlarının oluşması olası. Bu mesajların bu mesajları işleyen yapıların bu işlemlerini asenkron ve sırayla yapmasının sağlamak için de yapılar mevcut. Bunlardan son dönem en çok kullanılanı ise RabbitMQ.

Bir önceki yazımda RabbitMQ hakkında genel bilgileri vermeye ve kurulum işlemlerinin hem OnPremise hem de Docker üzerinde nasıl yapılacağını anlatmaya çalıştım. Bu yazıya aşağıdaki bağlantıdan ulaşabilirsiniz.

https://onnurkarakus.medium.com/nedir-bu-rabbitmq-28e3e9296ecd

Bu yazıda ise C# ile RabbitMQ’yu nasıl kullanacağız? konusunu incelemeye ve sizlere anlatmaya çalışacağım. C# ile iki Console uygulaması oluşturarak birinden gönderdiğimiz mesajların diğer uygulama üzerinden okunmasını sağlayacağız. Böylelikle temel olarak RabbitMQ işleyişini kontrol edebileceğiz. Uygulamamız için bir önceki yazımda anlatmaya çalıştığım Docker kurulumunu yaptığımız RabbitMQ yapısını kullanacağız.

O zaman başlayalım.

İlk önce Visual Studio üzerinden yeni bir Solution dosyası oluşturuyoruz. Daha sonrasında Console uygulamalarımızı bu Solution içerisinde oluşturacağız.

Uygulamalarımız Sender ve Receiver isminde iki console uygulaması olacak. Şimdi ilk olarak Sender uygulamamızı solution içerisinde oluşturalım. Uygulamamızın adı RabbitMQMessageApplication.Sender olsun.

Uygulamamızda RabbitMQ üzerinde oluşturacağımız kuyruğa bir sınıftan oluşturduğumuz nesnemizi göndereceğiz. Bu nesneyi RabbitMQ’ya göndermeden önce de Json ile serileştirmemiz gerekecektir.

Bu işlemleri yapabilmek için uygulamamıza aşağıda bulunan iki Nuget paketini eklememiz gerekmektedir.

Visual Studio içerisine bulunan Nuget Package Manager ile paketlerimizi ekleyebiliriz.

Şimdi uygulamamızı RabbitMQ üzerinde oluşturacağımız bir kuyruğa mesaj göndermesi için kodlarımızı ekleyelim.

İlk önce mesaj nesnemizi tanımlayalım. Bu nesnemiz kuyruğa gönderilecek ve başka bir uygulama üzerinden okunacak olan nesnemiz olacaktır. Sınıf ismini MessageInformation olarak veriyorum.

Daha sonrasında RabbitMQ için tanımları yaptığımız, kuyruk bilgilerini belirlediğimiz ve mesajımızı bu kuyruğa gönderdiğimiz kodları ekliyoruz.

Satır 12 : RabbitMQ bağlantımızı oluşturuyoruz. Burada kurulu olan adresimizi, kullanıcı adımızı ve şifremizi yazmamız yeterli olacaktır.

ConnectionFactory() RabbitMQ host uygulamasına bağlanmak için kullanılır.

Satır 13-14 : Burada RabbitMQ için bağlantımızı ve kanalımızı hazırlıyoruz.

CreateConnection() ConnectionFactory() ile tanımlarını yaptığımız host bağlantımızı oluşturuyor.

CreateModel() RabbitMQ üzerinden yeni bir kanal oluşturmak için kullanılır. Bu kanal sayesinde kuyruk bilgisi oluşturulacak ve mesajımız bu kuyruğa bırakılacaktır.

Satır 16 : Kanal tanımımız üzerinden kuyruk bilgilerimizi giriyoruz ve yeni bir kuyruk oluşturuyoruz.

QueueDeclare() Oluşturduğuz RabbitMQ tanımları içinde bir kuyruk oluşturmamızı sağlayacaktır.

durable : Kuyruğa gönderilecek mesajların saklama yöntemini belirler. Mesajlar InMemory olarak hafızada saklanabilirken istenirse fiziksel olarak da makine içerisinde saklanabilmektedir. Tabii burada hız, performans ve bilgilerin durumu önemli ölçüm değerleri olmaktadır. OnMemory kullanım eğer makine kapanırsa verilerin kaybolacağı anlamına gelmektedir. Bu yüzden kuyruk içinde iletilecek verilerin durumlarına göre bu tanım dikkatli bir şekilde kullanılmalı diye düşünüyorum.

exclusive : Kuyruğun bizim oluşturduğumuz bağlantı haricinde diğer Connectionlar ile de kullanılabileceğini belirler.

autodelete: Eğer kuyrukta bulunan mesaj bilgisini tüm tüketiciler (consumer) almış ise o zaman kuyruğun otomatik olarak silinmesi sağlanır. Yalnız burada dikkat edilmesi gereken konu eğer kuyruk içerisinden bilgiyi alacak bir consumer işlemi iptal edildi veya hata aldı ise o hakkının kaybedecektir. Bunun için autodelete özelliğini kapatıp kuyruk silme işlemi bir metot ile de yapılabilmektedir.

Satır 25 : Mesaj nesnemizi oluşturup gerekli bilgileri dolduruyoruz.

Satır 29 : Mesajımızı serileştirerek kuyruğa gönderilebilecek yapıya getiriyoruz.

Satır 32 : Mesajımızı RabbitMQ üzerinde oluşturduğumuz kuyruğa gönderiyoruz.

BasitPublish() : Hazırladığımız mesajın bir veya daha fazla kuyruğa aktarılmasını sağlar. Bir kuyruk tamam fakat birden fazla kuyruk konusu? Bu işlem için ExchangeType bilgileri kullanılmaktadır. ExchangeType bilgilerinin isimlerini aşağıda bulabilirsiniz. Kendileri ile ilgili yazıları da detaylı bir şekilde araştırdıktan sonra yazmaya çalışacağım.

  • Direct
  • Fanout
  • Headers
  • Topic

Uygulamamızı çalıştırdığımız zaman kullanıcıdan aldığımız mesaj bilgisinin RabbitMQ kuyruğuna gönderildiğini görebilmekteyiz.

Yine aynı şekilde RabbitMQ yönetim paneli içerisinden de kuyruk bilgilerimizi ve bu kuyruk içerisinde bulunan mesajların sayısal bilgilerine ulaşmamız mümkündür. Bunun için http://localhost:1360 adresine gidiyoruz ve guest@guest kullanıcı bilgileri ile login oluyoruz.

Burada da görebileceğiniz gibi MessageTest isminde bir kuyruğumuz ve onun içinde de Ready durumunda bekleyen bir adet mesajımız bulunmaktadır.

O zaman şimdi bu mesajı RabbitMQ üzerinden nasıl okuyacağımıza bakalım. Bunun için yine aynı solution içerisine RabbitMQMessageApplication.Receiver adından yeni bir console uygulaması ekliyoruz.

Projemize RabbitMQ işlemlerini yapabilmek için ve aldığımız mesajları geri açabilmek için aşağıdaki iki paketi Nuget ile ekliyoruz.

Şimdi yapmamız gereken RabbitMQ’ya bağlanarak Sender uygulaması ile oluşturduğumuz ve bir nesnemizi gönderdiğimiz kuyruğa bağlanarak mesajımızı almak. Serileştirilmiş bir şekilde bize gelen bu mesajı açarak artık istediğimiz yerde kullanabileceğiz.

Öncelikle Sender uygulaması içinde kullandığımız sınıfımızı buraya da tanımlayalım. Ortak bir proje içinden de alabilirdik ama benim tembelliğim oldu biraz.

Ardından Receiver uygulamamız için kuyruk bilgisini okuyan kod bloğumuz aşağıdaki gibi olacaktır.

Burada yine Sender uygulamamızdaki gibi ConnectionFactory üzerinden bağlantımızı oluşturduk. Kanal ve kuyruk tanımlarımızı yaptık.

Sender uygulamasından farklı olarak burada farklı olarak Received olayını (event) kullanıyoruz. Bu eventin kullanılma amacı sürekli olarak tanımlarının verdiğimiz kuyruğu dinleyerek yeni mesajların eklenmesi durumunda bu mesajları yakalayabilmektir.

BasicConsume() ile de gelen mesajımızı yakalayabiliriz. Burada kullandığımı autoAck parametresi uygulama içerisinden mesajı okuduktan sonra bu mesajın silinmesini sağlamaktadır.

İlk önce Receiver uygulamasının çalıştıralım ve Sender uygulamasının test ettiğimiz sırada eklediğimiz mesajın kuyruk üzerinden geldiğini görelim.

Şimdi isterseniz hem Sender hem de Receiver uygulamalarını aynı anda çalıştırarak gönderilen mesajın nasıl iletildiği kontrol edelim.

Kuyrukların Silinmesi
Oluşturduğumuz kuyrukların belirli durumlar karşısında silinmesini kendimiz metot kullanarak yapabiliriz. Daha önce de anlatmaya çalıştığım gibi otomatik bir silme mekanizması mevcut fakat bu consumer olan uygulamaların hataları karşısında mesajları almak haklarını kaybetmelerine sebep olabiliyor.

Kuyruk silme işlemini QueueDelete() ile yapabiliyoruz.

queue : Silmek istediğimiz kuyruk adıdır. 
ifUnused : Eğer kuyruk kullanılmıyor yani herhangi bir consumer yok ise o zaman silinsin anlamına gelmektedir. 
ifEmpty : Eğer kuyruk boş ise silinsin anlamına gelmektedir.

Evet bu yazımda RabbitMQ ile temel işlemlerin nasıl yapılacağı konusunu anlatmaya çalıştım. Umarım açıklayıcı olmuştur.

Bir başka makalede görüşmek üzere.

Kaynaklar :
https://www.rabbitmq.com/documentation.html
https://www.borakasmer.com/rabbitmq-nedir/
https://www.rabbitmq.com/dotnet-api-guide.html

Nedir bu RabbitMQ?

Merhaba,

Öğrenme isteği hem işimizin hem de hayatımızın önemli bir parçası. Bilgileri kovalamak, kovalayıp yakaladıkça işimize ve hayatımıza yenilikler katmak ve en önemlisi başarma duygusu. Bunların yeri çok önemli insan hayatında.

Tabii son yıllarda bilgiye ulaşmak özellikle internetin de yardımıyla kolaylaşmış durumda. Aradığımızı okumak için, okumak zor geliyorsa birilerinin anlatması için bir vidyo olarak bulmak artık çok kolay. Eskilere baktığımız zaman bir bilgiye ulaşmak, bir veriye ulaşmak o kadar kolay değildi. Yaşlıyım kabul edelim.

Benden büyüklerle (fakat çok büyüklerle) konuştuğum zaman bu bilgi alışverişinin zorluğunu bir nebze de olsa anlayabiliyorum. Örnek vermek gerekirse dedem. Aslında kendisi öğretmen fakat okuma aşkı yüzünden dışarıdan posta ile bir üniversite daha bitirmiş. Posta? Evet kitapların, sınavların ve diğer dokümanların posta ile gönderildiği bir üniversite yapısı.

Hem okuyan için zor bir süreç hem de okul için zor bir süreç aslında. Özellikle sınavlar konusu biraz karışık. Öğrencilere gönderilecek sınavların hazırlanması, bu sınavların öğrenciler tarafından tamamlandıktan sonra gönderilmesi ve okulda görevli kişiler tarafından kontrol edilip notlarının verilmesi. Şimdi baktığımız zaman elektronik ortamlarda bu işlemler kolay oluyor.

Fakat yazılımlarımız içerisinde de bu sorunlar yaşanmıyor mu? Yazılımların mesajlaşmaları, bu mesajların işlenmesi ve geriye cevaplarının dönülmesi tıpkı mektup ile yapılan sınavlar gibi zor olabilir mi? Dilerseniz bugün bu konuyu ve bu konu için sunulan güzel bir çözümü inceleyelim.

Geliştirdiğimiz uygulamalar, verilerin işlenmesinden sorumlu temel olarak. En küçük noktada düşünürsek uygulama genelinde mesajların işlenmesi (alınması ve cevaplanması) uygulama performansını etkileyen bir durumdur. Bu noktada bazı kullanılan işlemlerin anlık yapılması ihtiyacının olmadığının düşünerek bu işlemleri asenkron olarak yapmak isteyebiliriz. Bir örnekle konuyu açıklamaya çalışayım. Loglama. Bir API uygulamamızda gelen istek (request) ve cevapların (response) loglanması için middleware katmanın yazdığımız loglama işlemi her request ve response için uygulamıza bir yük oluşturacaktır. Peki, bu işlemleri asenkron bir şekilde yapsak? O zaman işte bize yardım için devreye RabbitMQ giriyor.

RabbitMQ açık kaynak kodlu bir mesaj kuyruk sistemidir. Şu an kullanılan RabbitMQ gibi farklı uygulamalar da mevcuttur. (Apache Kafka, Microsoft Azure Service Bus veya MSMQ)

Genel Özellikleri ile RabbitMQ?

  • Erlang dili üzerinde geliştirilmiştir.
  • Açık kaynak kodlu olduğu için yaygın bir kullanım alanına ulaşmıştır.
  • Cross platform yapısından dolayı farklı işletim sistemlerinde de kullanılabilmektedir.
  • Web arayüzü ile kullanım kolaylığı sağlamaktadır.

RabbitMQ Pusblish ve Subscriber mantığı ile çalışmaktadır. Bunun anlamı ise bir uygulamadan mesajı alıp sırası geldiği zaman bu mesajı başka bir uygulmaya iletmek olarak açıklanabilir.

RabbitMQ Genel Çalışması

Burada bazı terimlerin açıklamasına ihtiyaç olacağı için onları da açıklamaya çalışalım.

Publisher : Kuyruğa mesaj gönderen uygulamadır. 
Consumer : Kuyruktan gönderilmiş olan mesajı okuyan uygulamadır.
Queue : RabbitMQ içerisinde kullanılan mesajların alıp / verildiği kuyruk adıdır. 
Exchange : Routing Key bilgisine göre mesajların ilgili kuyruklara gönderilmesini sağlayan bilgidir. 
Exchange Type : Routing Key bilgisine göre mesajları hangi kuyruğa nasıl yönlendirilmesi gerektiğini belirleyen yapıdır.

RabbitMQ Kuralım

Ben iki farklı şekilde kurulum işlemlerini anlatmaya çalışacağım. İlk olarak her yerde karşımıza çıkan onpremise kurulumdan bahsetmek istiyorum.

RabbitMQ Erlang dilinde yazıldığından dolayı öncelikle bilgisayarımıza Erlang kurmamız gerekmektedir. Bunun için aşağıda bulunan bağlantıyı kullanabilirsiniz.

http://www.erlang.org/downloads

Erlang kurulumlarımızı tamamladıktan sonra RabbitMQ kurulum dosyaları için de aşağıdaki bağlantıyı kullanabilirsiniz.

http://www.erlang.org/downloads

Kurulumların ardından http://localhost:15672 adresi ile de RabbitMq yönetim panelini açabilirsiniz.

Bir başka kurulum yöntemi ise Docker kullanmak. Ben genellikle Docker üzerinde bu tarz uygulamalarımı kullanıyorum ve OnPremise kuruluma göre bir hayli avantajlı olduğunu düşünüyorum.

Gelin şimdi hep beraber Docker üzerinde RabbitMQ’yu nasıl kuracağımıza bakalım.

İlk olarak tabii ki Docker kurulumlarını tamamlamak gerekecek. Bu konu ile ilgili aşağıda bulunan makaleye bakabilir, kurulum işlemlerinin nasıl yapıldığını görebilirsiniz.

http://www.erlang.org/downloads

Docker kurulum işlemlerimizi tamamladıktan sonra RabbitMQ imajını indirmemiz gerekmektedir. Bunun için Powershell üzerinden aşağıdaki komutu yazmamız yeterli olacaktır.

docker pull rabbitmq

RabbitMQ imajını indirdikten sonra Container’ı hazırlamak ve çalışır hale getirmemiz gerekmektedir. Bunun için aşağıdaki komut satırının Powershell üzerinde çalışmamız yeterli olacaktır.

docker run -d -p 1360:15672 -p 5672:5672 — — name MyRabbitMQContainer rabbitmq:3-management

Bu komut ile iki farklı konfigürasyon düzenlemesi yapmaktayız. Bunlardan ilki Docker içerisinde çalışacak olan RabbitMQ 5672 portunun kendi lokal bilgisayarımızın 5672 portu ile eşliyoruz.

Yönetim uygulaması için de kullanılacak olan 15672 portunu da yine lokal bilgisayarımızın 1360 numaralı portuna eşiliyoruz. Böylelikle lokal bilgisayarımız üzerinden http://localhost:1360 adresine girdiğimiz zaman RabbitMQ yönetim paneline ulaşabileceğiz.

Daha sonra Docker Desktop uygulaması üzerinden çalıştırdığımız komutların hem imaj hem de container için çalıştığının görebiliriz.

RabbitMQ yönetim paneline giriş için ise lokal bilgisayarımızdan http://localhost:1360 adresine gitmemiz yeterli olacaktır. Açılan giriş ekranına aşağıdaki bilgiler ile giriş yapabilirsiniz.

Kullanıcı Adı: guest
Şifre: guest

Genel olarak RabbitMQ’nun ne olduğu ve kendi bilgisayarımız nasıl kurulacağı konusunda bilgiler aktarmaya çalıştım. Bir sonraki yazımda ise RabbitMQ ile mesajlarımızı nasıl yöneteceğimiz konusuna bakacağız.

Bir başka makalede görüşmek üzere.

Kaynaklar:
https://www.rabbitmq.com/
https://www.gencayyildiz.com/blog/dockerda-rabbitmq-ayaga-kaldirma/
https://www.borakasmer.com/rabbitmq-nedir/

ASP.NET Core Web Api ve Versiyonlama

Merhaba,

Uygulamalarımızın üretim ortamlarında çalışırken kontrolleri aslında genel olarak zor işlemler içeriyor. Tabii, bu işlemleri kolaylaştırmak için günümüzde önemli çözümler mevcut. Fakat en temel yaşadığımız sorunların başında yeni özelliklerin üretim ortamlarında devreye alınması gelmekte.

Aslında buraya kadar bir problem yok. Tamam, yeni özellikleri geliştir ve üretim ortamına al. Peki bu servisleri kullanan uygulamalar? Onların bu yaptığımıza tepkisi ne olur acaba? Büyük ihtimalle eğer ciddi değişiklikleri üretim ortamına çıkıyorsak hatalar alınacak ve projemiz belirli bir süre çalışmayacaktır. Böyle bir durumu da kimse istemez diye düşünüyorum.

Fakat, mevcut kullanıcıların bundan etkilenmemesini sağlamak tabii ki mümkün. Bu konuda kullanılan farklı çözümler ve yöntemler mevcut. Seçimin kişi, ekip, ekibin bilgisi ve tecrübesi ile doğru orantılı olduğunu düşünüyorum.

  • Farklı bir metot olarak üretim ortamına alalım ? (Gerçekten mi? API’yi kullanan uygulamalara ne diyelim?)
  • Bir parametreye bağlayabiliriz belki ?( Mevcut metodun kodlarında değişiklik mi? Ee test ihtiyacı? Hata olasılığı?)
  • Özellik açma / kapama (Feature Toggling) yapabiliriz? (Evet belki olabilir fakat parametre gibi olmayacak mı bir nevi ? Farklı uygulamalar mevcut denenebilir.)
  • Peki, versiyonlama yapsak ?

Versiyonlama, evet bugün bu konu hakkına bir şeyler yazmak istedim. ASP.NET Core Web API üzerinde versiyonlamayı nasıl yapabiliriz? Hangi yöntemler ile bu versiyonlama işlemlerini kullanabiliriz?

O zaman beraber ASP.NET Core Web API üzerinde versiyonlama işlemlerini nasıl yapıyoruz? Bakalım.

Önce yeni bir Web API projesi oluşturalım.

Visual Studio Code uygulamamızdan yeni bir terminal başlattıktan sonra aşağıdaki komut ile yeni WebApi projemizi oluşturalım.

Versiyonlama işlemlerimizi yapabilmek için bir Nuget paketine ihtiyacımız olacaktır. Aşağıdaki komut ile Microsoft.AspNetCore.Mvc.Versioning paketini projemize ekleyelim.

Paket kurulumundan sonra Startup.cs sınıfımızda aşağıda bulunan konfigurasyon tanımlarını yapmamız gerekmektedir.

Şimdi versiyonlama işlemlerimizi yapabiliriz.

Versiyonlama işlemleri birkaç yöntem ile yapılabilmektedir. Bu versiyonlama tekniklerini sırasıyla kontrol edelim.

API metodlarımızı değişikliğimizden sonra çağırdığımızda Header bilgileri içerisinde kullanılabilecek versiyon bilgilerini görüntüleme imkanına sahip oluyoruz.

URL Sorgusuna Dayalı Sürüm Oluşturma (URL Query Based Versioning)

Kullanılan URL adresi üzerinden versiyon bilgisi alınarak versiyonlama işlemleri yapılır. Aşağıda bulunan iki controller aynı API adresi üzerinden gelinen versiyon bilgisine göre çalışacaktır.

Göreceğimiz gibi burada farklı isimler ile tanımlanmış iki adet controller bulunmaktadır. Burada belirtilen route bilgileri ise aynıdır. API adresi olarak çağırdığımız api/home adresi bu farklılaştırmayı ApiVersion özelliği ile yapmaktadır. API adresine gönderilen query string bilgisi ile versiyon ayırımı yapılabilmektedir. Aşağıda iki versiyon için Postman üzerinden çağırım örneklerini bulabilirsiniz.

http://localhost:5000/api/home?api-version=2.0

Eğer API adresimizi bir versiyon bilgisi göndermeden çalıştırırsak o zaman varsayılan olarak 1.0 versiyonu gelecektir.

URL Yoluna Dayalı Sürüm Oluşturma (URL Path Based Versioning)

Genel yaygın kullanımlardan biri olan URL path kullanımı hem client uygulamaları için kullanımı kolay bir yöntemdir.

Postman üzerinden versiyon bazlı çağırımlarımız ise aşağıdaki gibi olacaktır.

http://localhost:5000/api/1.0/home

Http Başlık Tabanlı Sürüm Oluşturma (Http Header Based Versioning)

Görüldüğü gibi her versiyonlama yöntemi biraz daha kullanım bakımından daha iyi hale gelerek devam etmektedir. Tabii bu yöntemlerin client uygulama veya kullanılan yapıya uygun şekilde seçilmesi önemlidir. HTTP Header üzerinden versiyonlama kullanma yöntemi client uygulama tarafından gönderilen header bilgisi üzerinden versiyon bilgisini alarak çalışmaktadır.

Header üzerinden versiyon bilgisinin alınabilmesi için yapılandırmada ufak bir değişiklik yapmamız gerekecek. Bunun için Startup.cs sınıfımıza aşağıdaki kod satırını eklememiz gerekmektedir.

Kod değişikliğimiz ile API uygulamamızın versiyon bilgisini header üzerinden almasını sağlamış olduk.

Şimdi, Postman üzerinden header bilgisi olarak versiyon bilgimizi geçebilir ve sonuçları görebiliriz.

http://localhost:5000/api/home

Tek Controller Birden Fazla Action

Bazı durumlarda tek bir controller içerisinde bulunan action bilgilerinin de versiyonlanmasını isteyebiliriz. Böyle bir durumda controller içinde bulunan action metoduna MapToApiVersiyon özellik geçişini yapmamız yeterli olacaktır.

Header üzerinden alınan versiyon bilgisi ile yeni metodumuzun çalışması aşağıdaki gibi olacaktır.

API Versiyonlarını Kullanımdan Kaldırmak

API için hazırlanmış eski versiyonların kullanımdan kaldırılmasını (Deprecated) yine sınıf özelliği tanımları ile yapma imkanına sahibiz. Böyle bir durumda metodlarımızı API üzerinden çağırdığımızda kullanılmayan versiyon bilgilerine ulaşabiliriz.

Postman üzerinden metodumuzu çağırdığımız zaman ise Header alanında versiyonlar ile ilgili bilgileri görebiliriz.

Versiyon Tanımlarını Oluşturmak

Versiyon tanımlarının şimdiye kadar Controller veya Action bilgileri üzerinden tanımladık. Servis tanımlarını yaparken Controller versiyonlarının tanımlanmasını da sağlayabiliriz.

Eklediğimiz kod bloğunda hangi controller hangi versiyona sahip olacak bilgilerini tanımlayabiliyoruz. Controller bilgilerimiz ise aşağıdaki gibi olacaktır.

İstenilen API Versiyonunu Görüntüleyebilmek

Peki, böyle bir yapı içerisinde gelinen API versiyonunu nasıl alacağız? Versiyon bilgisini alarak istenilen durumlarda gelinen versiyon için de ayrı işlemler yapabiliriz. Bunun için GetRequestedApiVersion() metodundan yararlanabiliriz.

Postman çağırımlarımız ile metodların verdikleri cevaplar da aşağıdaki gibi olacaktır.

Bu yazımda API üzerinde nasıl versiyonlama yapabileceğimizi ve bu versiyonları nasıl yönetebileceğimizi biraz hem kendim anlamaya hem de sizlere açıklamaya çalıştım.

Versiyonlama yöntemleri birden fazla olduğundan dolayı ihtiyacımızı karşılayan uygun yöntem uygulamamızın yapısı, client kullanımları ve API kullanan diğer uygulamaların durumlarından dolayı farklılık gösterecektir.

Bir başka yazıda görüşmek üzere…