.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

Nedir Bu Docker?

Merhaba, uygulamalarımızı hazırlarken iş akışlarının oluşturmak, dizayn ve kodlama süreçlerini tamamlamanın yanında en önemli sorunlarda biri de uygulamanın nerede ve nasıl çalışacağının belirlenmesi adımıdır. Uygulamanın host edilmesi, çalışma zamanı kontrollerinin sağlanması, çalışılan uzak makinenin teknik altyapısının uygunluğu veya zaman içinde uygulamanın performans ihtiyaçlarına göre bu teknik özelliklerin artış / düşüş göstermesi kodlama aşamasından daha yorucu hale gelebilmektedir.

Özellikle son zamanlarda sıkça duyduğumuz mikro servis mimarisinin de hayatımıza girmesi ile birlikte dağıtık yapıların ismini daha çok telaffuz eder olduk konuşmalarımızda. Böyle bir durumda uygulamalarımızı nasıl ve nerede barındıracağız ve yöneteceğiz?

Bu makalemizde bu soruların cevaplarını bulmaya çalışırken Docker uygulamasının ne olduğundan, avantajlarından ve kullanımından bahsedeceğiz. Tabii bu arada ASP.NET Core uygulamalarımızı Docker ile nasıl yayınlayacağımız konusuna da değineceğiz.

Barındırma, hosting ve büyük zorluklar.

Hayatımıza bulut teknolojileri girmeden önce – yaşımdan dolayıASP barındırma hizmetlerine kadar gidebilirim fakat bunlarla canınızı sıkmak istemiyorum – barındırma hizmetleri için anlaştığımız firmalar ile çoğu zaman bir savaş veriyorduk. Ana makinelerin konfigürasyon ayarları, ek bellek veya işlemci ihtiyaçlarının sağlanması konularında tam bir kaos yaşanabiliyordu. Ardından gelen bulut teknolojileri ile birlikte bu sorunların büyük kısmının önüne geçmiş olundu. Hatta artık bu gibi ihtiyaçlarımızı birilerini arayarak veya e-posta göndererek değil kendimiz bir, iki küçük araç ile halleder duruma geldik.

Bu yeniliklerin için inceleceğimiz konu olan Docker da barındırma işlerimizi kolaylaştırmak adına bir araç olarak bize sunuluyor. Şimdi dilerseniz kavramları biraz inceleyelim ardından bu işlemleri nasıl yapılıyor beraber bakalım.

Docker mı ?

Docker uygulamasının temel amacı geliştirdiğimiz uygulamaların yayınlama ve çalıştırma süreçlerini kendi yapısında bulunan konteynerler (container) üzerinde gerçekleştirmesidir. Docker bu işlemleri yaparken bizleri çalıştığı işletim sisteminden soyutlayarak, her biri ayrı konteynerler üzerinde çalışan ve birbirlerini etkilemeyen uygulama grupları oluşturmamıza izin vermektedir.

Docker Mimarisi

Docker ilk olarak Linux ortamlar için geliştirilmiş ve daha sonrasında Windows ve MacOS işletim sistemlerine yayılmış bir uygulamadır. Uygulama çalışma mantığı olarak iki bölümden oluşmaktadır. Bunlardan ilki Linux çekirdeği ile direkt iletişim halinde olan Docker Deamon. İkinci bölüm ise kullanıcının yani bizim Deamon ile iletişim kurmamızı sağlayan Docker CLI.

Linux işletim sistemlerinde bu için bölüm direkt Linux ile haberleşirken Windows ve MacOS işletim sistemlerin Deamon bir sanal makine üzerinde koşturulmaktadır. Docker CLI ise yine kullanılan işletim sistemi üzerinde çalışmaya devam etmektedir.

Docker Deamon ve CLI iletişimi istem – sunucu yapısında çalışmaktadır. Deamon ile CLI soket veya REST API üzerinden iletişim kurabilmektedir. Bu sayede Docker Windows ve MacOS işletim sistemlerinde de çalışabilmesinin yanı sıra bir Docker CLI ile istediğimiz bir Docker Deamon’a da bağlanabilmekteyiz.

Docker Mimarisi ile ilgili görseli aşağıda bulabilirsiniz.

Birazda Docker ile beraber hayatımıza girecek olan yeni terimlere bakalım.

İmaj (Image) :

Docker tarafından çalıştırılacak olan konteynerlerin şemalarını temsil ederler. Konteynerlerin içerisinde bulunan işletim sistemini, dosya sistemini veya konteyner içerisinde kullanılacak olan uygulamaları barındırır. Eğer mevcutsa bir imaj başka bir imajdan kalıtım alarak da tanımlanabilir. İmaj dosyasının içeriği Dockerfile adı verilen bir dosya içerisinde tutulmaktadır. Dosyalar Docker hub içerisinde veya yerel ortamınızda depolanabilmektedir.

Konteyner (Container) :

Konteynerler bir imaj dosyasında oluşturulan tek bir ana bilgisayar içerisinde birden çok uygulamayı çalıştırmak için kullanılan yapılardır. Aslında bu işlem sanallaştırmaya benzer, ancak birden çok işletim sistemi oluşturmak için bir sunucuyu sanallaştırmak yerine konteynerler, işletim sistemini esasen sanallaştırarak daha hafif bir alternatif sunar. Böylelikle birden çok iş yükünün tek bir ana bilgisayarda çalışmasına izin verir. Her konteyner kendi dosya, ağ ve uygulama yapısına sahiptir.

Docker CLI (Command-Line Interface) – Docker İstemcisi :

Docker ile çalışırken kullanacağımız komut setlerini barındırmaktadır. Aslında temel olarak Docker Deamon ile kullanıcı arasındaki iletişimi sağlar. İmajların indirilmesi, indirilen imajlardan konteynerlerin oluşturulması ve bu konteynerlerin yönetilmesi Docker CLI ile yapılmaktadır.

Docker Daemon :

Asıl işi yapan kısımdır. Burada imajlardan oluşturulan konteynerlerin tüm yaşam döngüsünden sorumlu bölümdür. Dosya sisteminin kontrolü ve kaynakların sınırlandırılması (CPU ve RAM) burada yapılmaktadır.

Docker Hub :

İmaj dosyalarını indirip kullanabildiğimiz, yeni imaj dosyalarını ayarlayarak yükleyebildiğimiz bir alan olarak kullanılmaktadır.

Haydi Docker ile çalışalım!

Yazının ilerleyen bölümlerinde Docker ile bir imajın oluşturulması, bu imajdan bir konteyner oluşturulması ve bunun oluşturacağımız bir ASP.NET Core uygulaması yayınlanması konularına bakacağız.

İlk önce Docker Deamon ve CLI kurulumlarının nasıl yapıldığını inceleyelim.

Docker Kurulumu

Eğer bir Linux makine için kurulum işlemlerini yapacaksanız bu adresten adımları takip edebilirsiniz.

Eğer macOS bir makine için kurulum işlemlerini yapacaksanız bu adresten adımları takip edebiliriniz.

Kurulum işlemlerini Windows işletim sistemi üzerinden anlatmaya çalışacağım. .NET Core SDK kurulum işlemlerinin tamamlanmasının ardından Docker kurulum adımlarına bakabiliriz.

Öncelikle bu adresten kurulum dosyasının indirerek kurulum aşamalarını tamamlıyoruz.

Kurulum aşamaları tamamlandıktan sonra Docker Desktop uygulaması aşağıdaki şekilde olacaktır.

Docker üzerinden oluşturacağınız konteynerler için ihtiyacınız olan imaj dosyalarına Docker Hub üzerinden ulaşabileceğinizden daha önce bahsetmiştik. Bu işlem için öncelikle hub.docker.com adresine kayıt olmanız ve bir DockerId almanız gerekmektedir.

.NET Core MVC Uygulamamızı hazırlayalım.

İlk önce uygulamamızı hazırlayacağımız klasörü ve dotnet cli ile projemizi oluşturmamız gerekmektedir. Bunun için projemizi oluşturmak istediğimiz klasörü oluşturduktan sonra komut satırı editöründen oluşturduğumuz klasöre giderek aşağıdaki komut satırının çalıştırmamız yeterli olacaktır.

dotnet new mvc

Böylece projemiz için gerekli temel yapıyı oluşturuyoruz. Projemizi test etmek için ise komut satırı editörüne aşağıdaki satırı yazarak uygulamamızın hazır olmasını sağlayabilir, ardından uygulamamızı kontrol ettiğimiz zaman aktif bir şekilde çalıştığını görebiliriz.

dotnet run

Dockerfile diyorduk. Ne oldu ona?

Evet, uygulamamızın çalıştığını da gördükten sonra makalemizin ana konusu olan Docker ile uygulamamızı nasıl yayınlayacağımız konusunu inceleyeceğiz.

Öncelikle uygulamamızın Docker ile bağlantılı olabilmesi için bir Dockerfile oluşturmamız ve bu dosyanın tanımlarının yapmamız gerekmektedir. Uygulamamızın bulunduğu bir kod editörü ile açıp ardından Dockerfile tanımlarımızı yapmaya başlayabiliriz. Makale içerisinde editör olarak Visual Studio Code kullanılacaktır. VS Code ile uygulamamızın bulunduğu klasörü açabilmek için komut satırından uygulamamızın bulunduğu klasöre giderek code . komut satırının çalıştırmamız yeterli olacaktır.

Projemizin kök dizininde Dockerfile (büyük, küçük uyumuna dikkat etmemiz gerekiyor.) isminde bir dosya oluşturarak içeriğini aşağıdaki gibi tanımlıyoruz.

Dockerfile (büyük küçük uyumu önemli) Docker tarafından okunarak imaj dosyalarının oluşturulması sağlayan bir komut dosyası olarak düşünülebilir. Dockerfile içerisinde yapmak istediklerimizi biraz açıklamak gerekirse;

FROM : Docker konteynerinin hangi imaj örnek alınarak (kalıtım) oluşturulacağının tanımlandığı yerdir. Burada dikkat edilmesi gereken konu uygulamanız hangi .NET Core versiyonunda ise o versiyona uygun bir baz imaj seçmeniz gerektiğidir.

WORKDIR : Konteynerin çalışma dizin bilgisidir.

COPY : Kaynaktan hedefe kopyalanacak dosya bilgilerini içerir. Bu örneğimizde csproj dosyasının imaj dosya sistemi içerisine kopyaladıktan sonra dotnet restore komut satırı ile uygulamamızın çalışması için gereklilikleri yüklemiş oluyoruz.

RUN : İmaj içerisinde çalıştırmak istediğimiz komut bilgisidir. Burada dotnet publish ile uygulamamızı yukarıda belirttiğimiz yol ve özelliklere göre yayınlamış olacağız.

ENTRYPOINT : Docker konteyneri için bir çalışma bilgisi (executable) tanımlarını içerir. Burada bizim oluşturduğumuz proje konteyner için başlangıç noktası olacaktır.

Ve Sahne Docker’da

Şimdi ise uygulamamızın Docker içerisinde yayınlanması konusuna bakalım. İlk olarak komut satırı editörümüzden yine uygulamamızın çalışma dizininde iken aşağıda komut satırının çalıştırarak imajımızı derleyelim.

docker build -t hellodocker . 

Bu komut ile birlikte Docker tanımlara uygun bir şekilde gerekli olan imaj dosyalarının indirecek ve yeni bize ait olan imaj dosyamızı oluşturacaktır.

Ardından imajımızın doğru oluşturup oluşturulmadığı kontrol etmek için aşağıda bulunan komut satırını kullanabiliriz.

docker images

Eğer önceki ekranda bir problem ile karşılamadıysanız burada oluşturduğumuz imajı görebileceğiz. İmajın boyutu, id bilgisi ve ismi liste içerisinde gelecektir.

Bu işlemleri de tamamladıktan sonra artık oluşturduğumuz imajdan bir konteyner oluşturarak çalışır hale getirebiliriz. Konteyner ile birlikte de uygulamamız Docker üzerinde barındırılıp, yayınlanmış olacaktır. Bu işlemi yapabilmek için yine komut satırı editöründe aşağıda bulunan komutu çalıştırıyoruz.

docker run -d -p 8080:80 --name HelloDocker hellodocker

Biraz önce oluşturduğumuz konteynerin ve şu an Docker ile yayınlanan diğer konteynerlerin durumlarını öğrenmek için ise aşağıdaki komut yeterli olacaktır.

docker ps

Burada oluşturduğumuz konteynerin STATUS bilgisinin Up olduğunu görebilmekteyiz. Evet şimdi de uygulamamızı konteynerimizi aktif ederken belirttiğimiz port ve özelliklere göre kontrol edelim.

Gördüğünüz gibi uygulamamız artık Docker üzerinden yayınlanmaya başladı ve özelliklerini belirttiğimiz port üzerinden de bu kontrolü yapabiliyoruz.

Konteynerler ile işimiz belli bir süre sonra bitebilir ve aktif olarak yayınlanmasını hatta bunların Docker Registery içinde bulunmamasını istemeyebilir. Peki, mevcut konteynerleri nasıl durdurup, silebiliyoruz? Son olarak bu komutlara da göz atalım. Aşağıda bulunan komut ile çalışan bir konteyneri durdurabiliyoruz. Bunun için daha önceden kullandığımız docker ps komutu ile konteyner listesini alarak durdurma işleminde bu liste üzerinden bize verilmiş olan isimleri kullanacağız.

docker stop hellodocker

docker rm HelloDocker komut satırı ile konteynerimizi silebiliyor ve docker rmi hellodocker komut satırı ile de daha önceden oluşturduğumuz imaj dosyamızı silerek sistemden kaldırabiliyoruz.

Komut satırı ile yaptığımız bu işlemleri de yine Docker kurulumu ile beraber gelen Docker Desktop uygulaması ile de yapmamız mümkündür.

Evet, bir makalenin daha sonunda geldik. Bu makalede Docker nedir ? Docker ile bir uygulamamızı nasıl konteyner olarak barındırırız ve nasıl yönetiriz? konularının anlatmaya çalıştım. Umarın yararlı olmuştur.

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

Kaynaklar :

https://gokhansengun.com/docker-nedir-nasil-calisir-nerede-kullanilir/
https://medium.com/@teo.levy/getting-started-with-asp-net-core-docker-guide-for-beginners-d62af654881a
https://kancerezeroglu.wordpress.com/2016/06/17/docker/
https://medium.com/devopsturkiye/temel-d%C3%BCzeyde-docker-mant%C4%B1%C4%9F%C4%B1-ve-kavramlar%C4%B1-bde5418858d4

ASP.NET Core Web API ve Swagger

Merhaba,

ASP.NET Core ile birlikte çeşitli projeler geliştirebiliyoruz. Bu proje tiplerinden en önemlisi ise API projeler. API nedir? ASP.NET Core Web API bize neler sağlar? ve Swagger isimli ürün ile API’nin nasıl bir ilgisi olacak bu makalede bunları incelemeye çalışacağız.

Nedir bu API dedikleri?

API uzun adıyla Application Programming Interface, temel olarak farklı türden uygulamaların beraber çalışmalarını sağlayan yazılımlar olarak düşünülebilir. API uygulamalar arasında veri alışverişlerini sağlar. Tabii bu veri transferleri sırasında güvenlik, yetkilendirme ve yine genel uygulama mantığında kullanılan işlemlerin hepsi kullanılmaktadır. En belirgin fark ise API uygulamalarının bir arayüzlerinin (GUI) olmamasıdır.

API üzerinden alınan değerler ile -bazen de değer almadan- yapılan işlemlerin sonuçları (database crud, başka uygulamalardan veri almak vb.) başka uygulamalar tarafından okunabilir şekilde verilebilmektedir. Böylelikle farklı platformlarda çalışan, farklı teknolojilere sahip uygulamaların tamamı aynı API’ye veri gönderebilir ve sonuçları alarak kendi yapıları üzerinde değerlendirebilirler.

API teknolojilerinin günümüzde en bilinen kullanım yeri mobil uygulamaların arka tarafında bulunan servisler (backend) olarak karşımıza çıkar. Mobil uygulama ister IOS ister Android platformunda olsun aynı API üzerinden verileri okur ve işlemlerini yapabilir.

ASP.NET Core Web API ne oluyor o zaman ?

API aslında bir teknoloji, bir yapı olarak bahsedilen bir kavram. Tabii ki bir API uygulaması farklı diller ile yazılabilmektedir. Biz bu makalemizde ASP.NET Core ile bir API uygulamasının nasıl oluşturulacağının inceleyeceğiz.

ASP.NET Core ile ilgili daha içerikli bilgi için Nedir bu ASP.NET Core ?makalesini inceleyebilirsiniz.

ASP.NET Core Web API projemizi oluşturalım ardından Swagger nedir ? Nasıl kullanılır? İncelememize devam edelim.

Projemiz ile çalışmak için Visual Studio Code kullanacağız. Yeni bir ASP.NET Core projesi oluşturuyoruz. Projemiz için .NET Core CLI’den yaralanacağız. İlk önce webapi projemizi aşağıdaki gibi oluşturuyoruz.

Ardından uygulamamızı test etmek için dotnet run komutunu çalıştıralım ve oluşturulmuş olan API’mizi kontrol edelim.

Uygulamızın adresini komut çalıştırıldıktan sonra görebilmekteyiz. http://localhost:5000 bu adres üzerinden tarayıcımız ile uygulamamızı kontrol edebiliriz. Yeni oluşturulan iskelet uygulama içerisinde test edebilmemiz veya yeni başlayanlar için örnek olması açısında bir Controller hazır bir şekilde gelecektir. (WeatherForecast) bu controller üzerinden API’mizi test edebiliriz.

Bir başka test yöntemi ise API geliştiricilerini sıklıkla kullandığı POSTMAN uygulamasıdır. POSTMAN uygulamasını buradan indirebilirsiniz. POSTMAN, API’ler ile çalışmak için kullanılan bir uygulamadır. Tarayıcılar üzerinden API verilerini almanın veya isteklerin gönderilmesinin zorluğunu yaşamamak için POSTMAN kullanılmaktadır. POSTMAN üzerinden de API üzerinden bulunan controller aşağıdaki şekilde bize verileri getirmektedir.

Dilerseniz şimdi bir kod yazmaya geçebiliriz. Şimdi kendimiz bir controller oluşturarak buradan verilerin (statik olarak) gelmesini sağlayacağız. Ardından API üzerinde oluşturduğumuz bu controller’ı test edip Swagger ile çalışmamıza bakabileceğiz.

Yeni controller oluşturmak için Visual Studio Code kullanacağız projemizi Visual Studio Code ile açmak için yine komut satırı üzerinde code . yazmamız yeterli olacaktır.

Projemiz Visual Studio Code ile ilk açıldığı zaman uygulama projemizi build ve debug edebilmek için gerekliliklerin yüklenmesini isteyip / istemediğimizi bize soracaktır. Bu kısımda Yes butonu ile gerekliliklerin yüklemesini tamamlayabiliriz.

Yeni oluşturacağımız Controller temel araç bilgilerini API üzerinden servis edecektir. Bunun için ilk önce araç modelimizi oluşturmalıyız. Sol dosya gezgini üzerinden Models isminde yeni bir klasör açarak Car.cs isminde bir dosya oluşturuyoruz. Ardından bu dosyaya modelimizi yazıyoruz.

namespace ASPNETCoreSwagger.Models
{
    public class Car
    {
        public int Id { get; set; }
        public string Brand { get; set; }
        public string Model { get; set; }
        public int Year { get; set; }
    }
}

Ardından Controllers klasörü altında yeni bir dosya oluşturarak adına CarController.cs veriyoruz. Burada bir araç listesi ile ilgili işlemler yapacağız ve bu işlemleri API üzerinden servis edeceğiz.

CarController.cs dosyamız ise aşağıdaki gibi olacaktır.

using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc;
using ASPNETCoreSwagger.Models;
namespace ASPNETCoreSwagger.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class CarController : ControllerBase
    {
        [HttpGet]
        public IEnumerable<Car> Get()
        {
            return GetCars();
        }
        [HttpGet("{id}", Name = "Get")]
        public Car Get(int id)
        {
            return GetCars().Find(e => e.Id == id);
        }
        [HttpPost]
        [Produces("application/json")]
        public Car Post([FromBody] Car car)
        {
            return new Car() { Id = car.Id, Brand = car.Brand, Model = car.Model, Year = car.Year };
        }
        [HttpPut("{id}")]
        public void Put(int id, [FromBody] Car car)
        {
            
        }
        
        [HttpDelete("{id}")]
        public void Delete(int id)
        {
        }
        private List<Car> GetCars()
        {
            return new List<Car>()
            {
                new Car() { Id = 1, Brand = "Toyota", Model = "Auris", Year = 2012 },
                new Car() { Id = 2, Brand = "BMW", Model = "3.20", Year = 2019 },
                new Car() { Id = 3, Brand = "Renault", Model = "Clio", Year = 2020 }
            };
        }
    }
}

Şimdi oluşturduğunuz Controller’ı kullanarak API’mizi test edebiliriz.

Visual Studio Code üzerinden açacağımız terminal üzerinde dotnet run yazarak uygulamamızı çalıştıralım. POSTMAN üzerinden hem liste hem de istediğimiz araç bilgisine ulaşabilmekteyiz.

Buraya kadar API’mizin oluşturulması ile ilgili çalışmalar yaptık. Peki bu API’yi kullanacak olan uygulamaların geliştiricilerine API ve çalışması ile ilgili bilgiyi nasıl ileteceğiz ? Diyelim ki mobil uygulama geliştiren bir takımın içerisinde backend servislerini geliştiriyoruz. Mobil uygulamaları geliştiren ekip üyelerine bu geliştirdiğimiz metodların nasıl çalıştığı hakkında bilgi vermemiz gerekir. Hem metodların çağırılmaları hem de geri dönen cevap bilgilerinin doküman haline getirilip iletilmesi en sağlıklı yöntem olacaktır.

Böyle bir durumda her metodu tek tek belirli bir formatta yazmak elbette yorucu olacaktır. Bu durumu daha kolay ve pratik hale getirmek için Swagger ideal bir ürün olarak karşımıza çıkıyor.

Swagger eklediğimiz projemizde oluşturduğumuz metod bilgilerini otomatik olarak dokümante eder. Bu metod bilgileri her istekleri (request) hem de cevap (response) bilgileri ile birlikte API’mizi kullanmak isteyen uygulama geliştiriciler tarafından görüntülenebilir.

Swagger iki bölümden oluşmaktadır. Bunlardan ilki ve en önemlisi Swagger Specification‘dır. Burada Swagger tarafından otomatik olarak oluşturulan bir swagger.json dosyası mevcuttur ve her API güncellemesinde bu dosya güncellenerek API içerisinde bulunan metodlarımızın bilgilerini barındırır.

İkinci bölüm ise Swagger UI olarak adlandırılmaktadır. Burada da API üzerinde bulunan metodların görsel olarak bilgilerini alabiliriz. Metodların çalışmalarını, verdikleri cevapları ve aldıkları parametre bilgileri görüntülenebilir. Ayrıca basit bir şekilde metodları çalıştırabilir ve metod cevaplarının UI üzerinden alabiliriz. Swagger UI üzerinden özellikle bir uygulama yazarak API üzerinden metodların test edilmesine gerek kalmayacaktır.

Şimdi oluşturduğumuz uygulamamıza Swagger’ı nasıl uyarlarız ona bakalım.

Swagger üç bileşenden oluşmaktadır. Tüm bu bileşenleri tek nuget paketi şeklinde uygulamamıza almamız mümkündür.

Bu bileşenler;

  • Swashbuckle.AspNetCore.Swagger
  • Swashbuckle.AspNetCore.SwaggerGen
  • Swashbuckle.AspNetCore.SwaggerUI

Swagger paketini yükleyelim

Paketi projemize eklemek için Visual Studio Code terminal ekranından aşağıdaki komutu yazmamız yeterli olacaktır.

dotnet add package Swashbuckle.AspNetCore

Ardından paket uygulamamıza indirilmiş ve yüklenmiş olacaktır.

Swagger’ın uygulamamıza uyarlanması için ilk önce Startup.cs içerisinde bulunan ConfigureServices metoduna aşağıdaki satırları eklememiz gerekmektedir.

Yine Startup.cs içerisinde bulunan Configure metoduna da aşağıdaki satırları ekliyoruz.

Şimdi Visual Studio Code terminal ekranından uygulamamızı çalıştırıp Swagger uyarlamasının test edebiliriz. Bunun için terminal ekranına dotnet run yazmamız gerekmektedir.

Ardından tarayıcımızdan veya POSTMAN üzerinden https://localhost:5001/swagger/v1/swagger.json adresine girersek Controller bazlı metod bilgilerimizin json formatında geldiğini görebiliriz.

Peki bir UI bileşeninden bahsetmiştik. Şimdi de bu UI bileşenini nasıl kullanacağımıza bakalım.

Projemiz çalışırken https://localhost:5001/swagger/index.html adresine tarayıcımız üzerinden girdiğimiz zaman API Bilgileri başlıklı bir sayfa açılacak ve metot bilgilerimizi görüntüleyebileceğiz.

Swagger UI üzerinden metodların bilgilerini metot isimlerine tıklayarak alabiliriz.

Burada araç bilgilerini listelediğimiz metodumuzun özelliklerini görüyoruz. Parameters bölümü metodumuzun parametre bilgilerini, Responses bölümü ise API metodu üzerinden dönen cevap (response) hakkında bilgi vermektedir. Sağ üst kısımda bulunan Try it out butonu ile API metodunu çalıştırabilir dönen bilgileri de görebiliriz.

Swagger UI üzerinde metodlarımızın açıklamalarını da görüntülememiz mümkündür. Bu şekilde metodlarımızı inceleyen uygulama geliştiricilere anlamlı açıklamalar ile metodun görevini anlatmamız mümkün olmaktadır.

Bunun için uygulamamızın csproj dosyasına (bizim uygulamamız için dosya adı ASPNETCoreSwagger.csproj) aşağıda bulunan satırları eklememiz gerekmektedir.

Ardından Startup.cs dosyasında bulunan ConfigureServices metoduna da aşağıdaki satırları ekliyoruz.

Artık API metodlarımıza açıklamalar ekleyebilir ve bu açıklamaların Swagger UI üzerinden görüntülenmesini sağlayabiliriz. Metodlarımıza nasıl açıklama ekleyeceğimizin örneğini aşağıda bulabilirsiniz. Ardından Swagger UI üzerinden metot açıklamasını görüntüleyebilirsiniz.

Projemizi yeniden çalıştırıp Swagger UI üzerinden kontrol ettiğimiz zaman metod açıklamasının geldiğini görebiliriz.

Görüldüğü gibi Swagger API projelerimizde dokümantasyon işlemlerini bizler için hızlı bir şekilde çözmektedir. API’den yararlanacak olan yazılım geliştiricilerin doğru bilgilere ulaşması Swagger ile daha rahat olacaktır.

Başka bir makale ile yeniden görüşmek üzere.

Kaynaklar:
https://www.yazilimbilisim.net/c-sharp/asp-net/api-nedir/
https://code-maze.com/swagger-ui-asp-net-core-web-api/

Visual Studio Code kullanarak ASP.NET Core Projesi Geliştirme

Gereksinimler ve kurulumlar

Visual Studio Code Microsoft tarafından ücretsiz olarak dağıtılan, cross-platform olarak çalışan, Windows, Mac ve Linux işletim sistemleri için geliştirilmiş bir kod editörüdür. Microsoft Visual Studio bir IDE olarak çalışırken Visual Studio Code bir kod editörüdür. Yalnız, VS Code da Visua Studio gibi uygulama geliştirme işlemleri için gayet kullanışlı bir üründür.

Bu makalemizde VS Code kullanarak bir ASP.NET Core uygulamasının geliştirme, debug, test ve dağıtım işlemlerinin nasıl yapıldığına bakacağız.

Visual Studio Code uygulamasının indirmek için aşağıdaki bağlantıyı kullanabilirsiniz.
https://code.visualstudio.com/

Visual Studio Code kurulumunu tamamladıktan sonra ASP.NET Core projelerimizi oluşturup kullanabilmemiz için bazı uzantıların (extension) kurulumlarını yapmamız gerekmektedir. Bunun için VS Code uygulamasının açtıktan sonra Ctrl + Shift + X tuş kombinasyonu ile veya sol menüde bulunan Extensions ikonuna tıklayarak açılan ekranda “C#” aramasının yaparak çıkan ilk uzantıyı Install butonu ile kurulum işlemini tamamlayabiliriz.

Ardından aynı şekilde Nuget Gallery için uzantı kurulumunu tamamlamamız gerekmektedir.

VS Code genel olarak ASP.NET Core projelerini oluşturmak için Visual Studio gibi bir komut yapısına sahip değildir. .Net Core CLI bu konuda yazılımcılara yardımcı olmaktadır. Biz de projemizi .Net Core CLI üzerinden oluşturacağız.

İlk ASP.NET Core Uygulamamız

İlk önce projemizin oluşturulacağı dizini oluşturmamız gerekmektedir.

Ardından oluşturduğumuz klasörü VS Code ile açmamız gerekmektedir. Bunun için File menüsünden Open Folder… komutu seçilmesi yeterli olacaktır.

VS Code kendi içerisinde bir terminal yapısı barındırmaktadır. Böylelikle komut satırı kullanmadan işlemlerimizi bu terminal yapısı ile gerçekleştirmemiz mümkün olacaktır. Terminal menüsünü açmak için Terminal menüsünden New Terminal (Ctrl + Shift + “) komutunu seçmemiz yeterli olacaktır.

Terminal alt kısımda açıldıktan sonra aşağıda bulunan komut ile ilk ASP.NET Core projemizi oluşturmaya başlayabiliriz.

dotnet new mvc

Yukarıda bulunan komut Terminal ekranı üzerinde çalıştırıldığı zaman bulunduğumuz klasör içerisinde yeni bir Asp.Net Core MVC projesi oluşturulacak ve dotnet restore komutu otomatik olarak çalıştırılıp paket bilgileri de güncellenecektir.

Projemizin çalıştırılması

Projemizi çalıştırmak için yine .NET Core CLI komutlarından faydalanacağız. Bunun için terminal ekranına aşağıdaki komut yazılarak çalıştırılabilir. Böylelikle projemiz bize bilgileri verilecek olan localhost adresi ve bir port üzerinden çalışmaya başlayacaktır.

dotnet run

http://localhost:5000 adresine bir web tarayıcı ile girdiğimiz zaman projemizin çalıştığının görebiliriz.

Uygulamamızın VS Code ile birlikte açılması için (F5) ve uygulamamızı debug edebilmek için launch.json ve tasks.json dosyalarının oluşturulmasına ihtiyacımız vardır. Bu dosyaları oluşturmak için VS Code’un bir özelliğini kullanabiliriz. Bunun için projemizin olduğu klasörü VS Code ile yeniden açmamız gerekmektedir. VS Code projemizi kontrol edecek ve bize gerekli dosyaların oluşturulup oluşturamayacağının soracaktır. Oluşturulan dosya örneklerini aşağıda bulabilirsiniz.

Artık Run menüsünde bulunan Start Debugging komutu ile de projemizi çalıştırma imkanına sahip olmaktayız. Böylelikle projemiz içerisinde break point’ler kullanarak projemizi debug edebiliriz.

tasks.json ve launch.json dosyalarının VS Code üzerinden oluşturmak istersek;

Komut panelini açtıktan sonra (Ctrl + Shift + P) ardından palet içerisinden Tasks: Configure Task komutunu seçmemiz gerekmektedir. Ardından Create tasks.json file from template komutu ve gelen liste üzerinden .NET Core seçmemiz yeterli olacaktır.

launch.json dosyası için de sol bölümde bulunan Run (Ctrl + Shift + D) ekranından create a launch.json file seçeneğini seçmemiz yeterli olacaktır. Ardından projemizi çalıştırabilir ve debug işlemlerimizi yapabiliriz.

Bir başka makale ile yeniden görüşmek üzere.