Monolitik yapıda, .Net Core 8 ile geliştirdiğim twitter (x) clone projesi. Projeyi yapmamdaki ana neden dotnet ile katmanlı mimari öğrenmekti. Bunu dışında bir API de olması gereken diğer özelikleride (Rate Limit, Error Handiling, Versioning vs.) öğrenmiş oldum. Bu öğrendiklerimi bölümler halinde kısaca açıklayacağım.
Her sorguda tüm verileri göndermek yerine bu verileri sayfalandırarak kullanıcıya sunmaktır. Bunu projede şu şekilde yapıyoruz: List sınıfını generic olarak miras alan PagedList sınıfını oluşturuyoruz. Bu sınıf gelen veri listesini RequestParameters türünde gelen parametreler ile sayfalandırıyor ve toplam verinin ne kadar olduğu, kaç sayfa olduğu gibi verileri hesaplayıp MetaData sınıfından sahip olduğu MetaData değikenine atıyor. Artık gelen her istekte gelen veri listesi bu şekilde sayfalanmış oluyor (Örn: GetAllFollowersAsync).
API'nin versiyonlaması, adından da anlaşılacağı gibi API'ı versiyonluyoruz. Bir endpoint'in geliştirilmesi, yeni özellikler eklenmesi gibi durumlarda versiyonlama kullanılır. Versiyonlama şu şekildedir:
- Öncelikle bağımlılıkları yüklüyoruz (Asp.Versioning.Mvc ve Asp.Versioning.Mvc.ApiExplorer).
- Program.cs de gerekli konfigürasyonları yapıyoruz. Bunu doğrudan Program.cs' e yazmadım, onun yerine extension olarak ServicesExtensions 'e ekledim. Burada kısaca varsayılan sürümün ne oalcağı, sürüm belirlenemse ne yapılacağı, sürümün hangi formata ve nerelerden (Header, Url, Query) alınacağı gibi ayarlamaları yaptık.
- Controllerl'lardaki örnek kullanım: TweetController. API Url:
BaseUrl/api/v1/Tweet
veyaBaseUrl/api/v2/Tweet
gibi dir.
Yazılım çalışırken oluşan durumları kaydederiz ve buna loglama diyoruz. Bu durumlar hatalar, uyarılar, bilgi mesajları vs. olabilir. Örneğin uygulamada oluşan bir hatayı loglama sayesinde daha hızlı tespit edip hatanın nereden ve ne zaman kaynaklandığını bulup ona göre bir reaksiyon alabiliriz. Bunu dışında logları uygulama dışında depolamak, analiz etmek veya sorgulamak için analiz motorları kullanılabilir. Buna örnek olarak Elasticsearch verilebilir. Bu projede de bunu kullandım. Logları hem API üzerinde ilgili dosyada tutuyorum hem de Elasticsearch' e göderiyorum. Loglama için LoggerService ve Serilog' un konfigürasyonu appsettings.json da mevcutur.
Uygulamada çıkan bir exception' nın nasıl ele alınacağını belirleriz. Temel olarak bu işlem şu şekildedir:
- Projedeki ExceptionMiddlewareExtensions yer aldığı gibi
UseExceptionHandler
ile istisnaları yakalar ve işleyiciyi çalıştırırız. - Oluşan Hatanın özeliklerini alırız (IExceptionHandlerFeature).
- Hatanın custom olarak oluşturduğumuz
Exception
class'lardan mı? yoksa farklı bir durumdan mı? olduğunu kontrol eder ona göre StatusCode ayarlarız ve yanıtı göndeririz. Burada sadece hatayı işleyip response dönülmez aynı zamanda bu hata loglarınır. - Son olarak Program.cs' de bu middleware çağırıyoruz.
Servislerin belirli bir zaman diliminde alabileceği isteklerin sayısını sınırlamaktır. Bu kısıtlama aşırı yüklenmeyi önlemek, hizmet kalitesini korumak ve güvenlik açıklarını engellemek için kullanılır. .Net Core' da kullanmak için:
- AspNetCoreRateLimit bağımlılığını yüklüyoruz.
- appsetings.json'a ilgili konfigürasyonu ekliyoruz (bunu bu şekilde yapmak yerine ilgili ayarları doğrudan statik olarak da yazabilirsiniz).
- ServicesExtensions de bu konfigürasyonları ve diğer servislerin ayarlamalarını yapıyoruz.
- Bundan sonra tek yapmamız gereken bu methodu Program.cs'de çağırmak ve Program.cs' de Ip tabanlı rate limiting'i etkinleştirmek.
- Artık API' ye rate limiting özelliği eklenmiş olacaktır. Örneğin benim konfigürasyonuma göre her client saniyede en fazla 5 istek atabilir eğer bu sınırı aşılırsa HTTP codu 429 olan bir hata alır.
Bir uygulamanın veya servisin sağlığını izlemek ve durumunu belirtmek için yapılan düzenli kontrollerdir. Bu Projede API' nin durumunu ve diğer dış servislerin durumlarını kontrol etmek için kullandım. Konfigürasyon genel olarak şu şekildedir:
- Dış servislerin daha önce yazılmış HealthChecks kütüphaneleri (AspNetCore.HealthChecks.SqlServer, .Redis, .Elasticsearch vs.) projeye dahil edilir.
- Çıktıyı daha istediğimiz gibi ayarlamak için UI.Client da dahil edilir.
- Serviclerin HealthChecks konfigürasyonları eklenir. Bu konfigürasyonlar şu şekildedir:
- Bağalntı noktası için
connectionString
veyaUri
- Servise göre
healthQuery
- Check response' nda servise verilen isim (Bu isim unique olmalı)
name
- Servis ulaşılamadığı zaman ki durum değeri
failureStatus
- Son olarak tag'ler
tags
- Bağalntı noktası için
- Healt Check rotasını Program.cs tanımlıyoruz. aynı zamanda
HealthCheckOptions
ile response' daki diğer ayarlamaları da yapabiliyoruz (Örneğin: Exception'nın hata mesajını ayrıntılı olarak verme gibi). - İşlemler tamamlanmış oldu. Artık API' nin
BaseUrl/health
Url'ine istek atığımızda API' nin ve external servislerinin durumunu görebileceğiz. - Bu projedeki örnek Health Check response:
{ "status": "Healthy", "totalDuration": "00:00:00.0030152", "entries": { "SQL Server Check": { "data": {}, "duration": "00:00:00.0024416", "status": "Healthy", "tags": [ "sql", "sql-server" ] }, "Redis Check": { "data": {}, "duration": "00:00:00.0026851", "status": "Healthy", "tags": [ "cache", "redis" ] }, "Minio Check": { "data": {}, "duration": "00:00:00.0024188", "status": "Healthy", "tags": [ "file", "minio" ] }, "Elasticsearch Check": { "data": {}, "duration": "00:00:00.0029169", "status": "Healthy", "tags": [ "log", "elasticsearch" ] } } }
Veri tabanına verileri kaydetmeden önce bu verilerin bizim istediğimiz veya veri tabanındaki özelikleriyle uyuşup uyuşmadığını kontrol etmek zorundayız. Bunu yapmasak büyük ihtimale haat alırız. Bu yüzden farklı validasyon yöntemleri mevcutur, isterseniz kendiniz her bir model veya Dto (Data Transfer Object) için ayrı ayrı validasyonlar yazabilirsiniz, veri anotasyonları (Data Annotations) ile bunu yapabilir veya bu işi yapan hazır kütüphanelerden de yararlanabilirsiniz. Ben bu projede güçlü bir kütüphane olan Fluent Validation kullandım. Kullanımı şu şekilde:
- FluentValidation bağımlılığını projeye dahil ediyorsunuz.
- Daha sonra ilgili validator'larımızın nerede olacağını Program.cs' de belirtmemiz gerekiyor. Presentation katmanına referans için AssemblyReference adında bir class ekeldim ve bunu Program.cs'de projeye entegre etim.
- Artık tek yapmak gereken
Presentation
katmanında bu validasyon sınıflarını oluşturmak. - Örneğin kullanıcı kayıt Dto' sunun validator'ını incelecek olursak:
- Öncelikle Fluent Validation' nın generic
AbstractValidator
sınıfını ilgili Dto' muz ile beraber miras alıyoruz. - Daha sonra kurucu methodumuz içinde ilgili kuraları belitiyoruz. Örneğin
FullName
alanının NULL ve Empty olamayacağını, uzunluğunun minimum 5, maksimum 100 olabileceğini belirtiyoruz. Eğer bu kuralara uymayan bir değer gelirse fluent validation' unIsValid
değeri false olacak ve kullanıcıya ister custom ister default hata mesajını dönecektir.
- Öncelikle Fluent Validation' nın generic
- Validasyonu menüel veya otomatik yapmak mümkündür. Ben projede manüel validasyonu kullandım. Örneğin kullanıcı kayıt endpoint i de
[FromServices] IValidator<UserDtoForRegister> validator
şeklinde methoda ekliyoruz. Artık bu endpoint e bir istek yapıldığındaUserDtoForRegister
' nın validator'ı varsa bunuIValidator<T>
ile enjekte edrek validasyon işlemini manüel olarak gerçekleştirebiliyoruz.
Geliştirilen projenin test edilebilir en küçük parçasının test edilmesi işlemidir. Bu konu hakkında daha önce bir medium makalesi yazdığım için burada anlatmayacağım. İsteyenler blog yazımı okuyabilirler.
GitHub' da bulunana projenizde gerçekleşen herhangi bir eylemde başka bir işlem tetikleyebildiğiniz bir platformdur. Örneğin repo' nun main
branch' ine bir commit atıldığında veya bir merge işlemi yapıldığında uygulamayı build edebilirsiniz ya da benim bu projede yaptığım gibi main
branch' inde yapılan herhangi bir değişilikte unit teslerinizi çalıştırıp bunu durumunu da Read.md dosyanızda badge olarak gösterebilirsiniz. Örnek badge bu dosyanın en üst kısmındadır. Bu işlemleri birleştirmek de mümkündür, örneğin main
branch' ine bir merge işlemi yapıldığında gidip önce unit tesleri çalıştırıp eğer unit testler geçerse projeyi build edip veya paketleyip başka bir platforma push' layabilirsiniz.
Projeye eklemek şu şekildedir:
- Projeye
.github
ve onun içinedeworkflows
adında bir klasör oluşturuyoruz. WorkFlows dosyasına YML formatında iş akışlarımızı ekliyoruz. Örnek dosya unit-test.yml:
name: build and test # Akışın adı
on:
push: # main branch' ine bir push veya pull request işlemi yapıldığında ve .cs veya .csproj uzantılı dosyalarda değişiklik olmuşsa çalışacağını belirtiyoruz
pull_request:
branches: [ main ]
paths:
- '**.cs'
- '**.csproj'
env:
DOTNET_VERSION: '8.0.x'
jobs:
build-and-test: # Job tanımlama
name: build-and-test-${{matrix.os}} # İşlemi farklı işletim sistemleri üzerinde çalıştırma.
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
steps:
- uses: actions/checkout@v3 # repo' nun dosyalarını işlem yapılacak konuma kopyalar.
- name: Setup .NET Core
uses: actions/setup-dotnet@v3 # .Net Core' un yapılandırılması
with:
dotnet-version: ${{ env.DOTNET_VERSION }}
- name: Install dependencies
run: dotnet restore # Bağımlılıkların yüklenmesi
- name: Build
run: dotnet build --configuration Release --no-restore # projeyi derleme
- name: Test
run: dotnet test --no-restore --verbosity normal # testlerin çalıştırılması
Projelerde dosya işlemleri birçok şekilde yapılabilir. Bunlar; dosyayı binary olarak veri tabanına kaydetmek, dosyayı proje üzerinde bir path
' e kaydetmek (en tehlikeli ve old school olanı) veya external bir servisde tutmak. Her senaryonun kendine göre ve proje özelinde avantajları ve dezavantajları vardır (API üzerinde tutmayın !!). Ben doğrudan daha çok kullanıldığını gördüğüm yöntemi kullandım yani dosyaları dış bir serviste tutum. Bu servis Minio, minio open source bir nesne depolama sistemidir. Ayrıca Amazon S3 ile API uyumludur.
Entegrasyonu:
- Minio bağımlılığını projeye dahil ediyoruz.
- appsettings.json' da gerekli configürasyonları giriyoruz (Bunları statik olarak kod içinde de yazabilirsiniz).
- ServicesExtensions' da gerekli ayarlamalar yapılır ve Program.cs ' de bu extension eklenir.
- Artık kullanmak istediğiniz serviste
IMinioClient
' ı enjekte etmeniz yeterli olacaktır (Diğer işlemlerin kodları FileUploadManager ve FileDownloadManager' da mevcutur oradaki kodları inceleyebilirsiniz.)