ABP Framework ile Adım Adım Yazılım Geliştirme – Bölüm 4: Domain Layer Geliştirme
Domain Layer (Domain Katmanı), uygulamanızın çekirdek iş mantığını ve kurallarını barındıran en önemli katmanıdır. Bu katman, veritabanı veya UI gibi dış detaylardan bağımsızdır ve uygulamanın işleyişinin kalbidir. Bu bölümde, domain katmanınızı zenginleştirecek anahtar kavramları inceleyeceğiz.
Domain Service’ler Oluşturma
Domain Service (Domain Servisi), tek bir entity’ye ait olmayan, ancak birden fazla entity’yi veya aggregate root’u içeren iş mantığı için kullanılır. Genellikle bir fiili veya bir işlemi temsil eder.
Örnek: İki kitabı birleştiren veya bir kitabı başka bir kategoriye taşıyan bir domain servisi.
C#
using System;
using System.Threading.Tasks;
using Volo.Abp.Domain.Services;
using Volo.Abp.Domain.Repositories;
namespace MyProject.Books
{
public class BookManager : DomainService
{
private readonly IRepository<Book, Guid> _bookRepository;
public BookManager(IRepository<Book, Guid> bookRepository)
{
_bookRepository = bookRepository;
}
public async Task ChangeBookTypeAsync(Book book, BookType newType)
{
if (book.Type == newType)
{
return; // Tür zaten aynıysa işlem yapma
}
// İş kuralı: Belirli bir türden diğerine geçiş kısıtlaması olabilir
if (book.Type == BookType.Horror && newType == BookType.Adventure)
{
throw new BusinessException(MyProjectDomainErrorCodes.HorrorToAdventureNotAllowed);
}
book.Type = newType;
await _bookRepository.UpdateAsync(book);
}
// Başka domain işlemleri...
}
}
DomainService
sınıfından türeyerek servislerinizdeki bağımlılık enjeksiyonunu kolaylaştırırsınız.- Domain servisleri genellikle
IRepository
arayüzleri aracılığıyla entity’lere erişir.
Business Logic Implementasyonu
Domain katmanında iş mantığını implemente ederken, entity’lerin kendi iç tutarlılıklarını korumalarını sağlamak önemlidir. İş kuralları mümkün olduğunca entity’lerin kendi metotları içine yerleştirilmelidir. Eğer iş kuralı birden fazla entity’yi ilgilendiriyorsa, o zaman bir domain servisi kullanılabilir.
Örnek: Book
entity’si içinde bir iş kuralı:
C#
public class Book : AggregateRoot<Guid>
{
public string Name { get; private set; } // Private setter ile kontrollü değişim
public BookType Type { get; set; }
public DateTime PublishDate { get; set; }
public float Price { get; set; }
// Yapıcı metod
private Book() { /* EF Core için boş yapıcı metod */ }
public Book(Guid id, string name, BookType type, DateTime publishDate, float price)
: base(id)
{
SetName(name); // İş kuralı uygulama
Type = type;
PublishDate = publishDate;
Price = price;
}
public void SetName(string name)
{
if (string.IsNullOrWhiteSpace(name))
{
throw new ArgumentException("Book name cannot be empty.", nameof(name));
}
if (name.Length > 128)
{
throw new ArgumentException("Book name cannot exceed 128 characters.", nameof(name));
}
Name = name;
}
}
Domain Event’ler
Domain Event (Domain Olayı), domain içindeki bir şeyin olduğunu ve diğer domain bileşenlerinin ilgilenebileceği bir olayı temsil eder. Domain olayları, farklı aggregate root’lar veya modüller arasında gevşek bağlı iletişimi sağlamak için kullanılır.
ABP Framework, Event Bus (Bkz. Bölüm 11) mekanizması ile domain olaylarını kolayca yayınlamanıza ve dinlemenize olanak tanır.
- Domain Event Tanımlama:
Volo.Abp.EventBus.Distributed.IDistributedEvent
veyaVolo.Abp.EventBus.Local.ILocalEvent
arayüzlerini implemente eden bir DTO sınıfı. C#public class BookCreatedEto // ETO: Event Transfer Object { public Guid BookId { get; set; } public string BookName { get; set; } // Diğer ilgili veriler }
- Olay Yayınlama: Bir domain servisi veya entity içerisinde
IDomainEventBus
servisi aracılığıyla olay yayınlanır. C#// BookManager içinde public async Task CreateBookAsync(string name, BookType type, ...) { // Kitap oluşturma mantığı... var book = new Book(...); await _bookRepository.InsertAsync(book); // Olayı yayınla await _eventBus.PublishAsync(new BookCreatedEto { BookId = book.Id, BookName = book.Name }); }
- Olay Dinleme (Handler):
IEventHandler<TEvent>
arayüzünü implemente eden bir sınıf. C#public class BookCreatedEventHandler : IEventHandler<BookCreatedEto>, ITransientDependency { public async Task HandleEventAsync(BookCreatedEto eventData) { // Olay meydana geldiğinde yapılacak işlemler Console.WriteLine($"Yeni kitap oluşturuldu: {eventData.BookName} (ID: {eventData.BookId})"); // Örneğin, bir loglama, bildirim gönderme veya başka bir işlemi tetikleme await Task.CompletedTask; } }
Validation Kuralları
Domain katmanında iş kurallarını (business rules) ve validasyonları uygulamak, uygulamanızın veri bütünlüğünü sağlar. ABP, FluentValidation gibi popüler kütüphanelerle entegre edilebilir veya basit if
blokları ve BusinessException
ile kendi validasyonlarınızı uygulayabilirsiniz.
C#
using Volo.Abp; // BusinessException için
public class Book : AggregateRoot<Guid>
{
public void SetPrice(float price)
{
if (price <= 0)
{
throw new BusinessException(MyProjectDomainErrorCodes.BookPriceMustBePositive);
}
Price = price;
}
}
Value Objects Kullanımı
Value Object (Değer Nesnesi), kimliği olmayan, sadece değerleri ile tanımlanan bir nesne türüdür. İki değer nesnesi, değerleri aynıysa eşittir. Örnek olarak bir adres, para birimi veya renk gibi kavramlar değer nesneleri olabilir.
ABP’de değer nesneleri için özel bir taban sınıf yoktur, ancak .NET record tipleri veya özel bir sınıf ile implemente edilebilirler.
C#
// Örnek bir Value Object
public class Address
{
public string Street { get; private set; }
public string City { get; private set; }
public string ZipCode { get; private set; }
public Address(string street, string city, string zipCode)
{
Street = street;
City = city;
ZipCode = zipCode;
}
// Değer karşılaştırması için eşitlik (Equality) override edilmeli
public override bool Equals(object obj)
{
if (obj == null || GetType() != obj.GetType())
{
return false;
}
var other = (Address)obj;
return Street == other.Street && City == other.City && ZipCode == other.ZipCode;
}
public override int GetHashCode()
{
return HashCode.Combine(Street, City, ZipCode);
}
}
Bu değer nesnesini bir entity içinde kullanabilirsiniz:
C#
public class Author : AggregateRoot<Guid>
{
public string Name { get; set; }
public Address HomeAddress { get; set; } // Adres bir değer nesnesi
}
Specification Pattern Uygulaması
Specification Pattern (Şartname Deseni), iş kurallarını sorgu mantığından ayırmak için kullanılır. Belirli bir iş kuralını karşılayan entity’leri filtrelemek için esnek ve yeniden kullanılabilir yollar sağlar.
ABP, doğrudan bir “Specification” yapısı sağlamaz, ancak Expression kullanarak veya özel IQueryable
uzantıları yazarak bu deseni uygulayabilirsiniz.
C#
using System;
using System.Linq.Expressions;
using Volo.Abp.Domain.Entities;
// Örnek bir spesifikasyon (bir sınıf içinde de tutulabilir)
public static class BookSpecifications
{
public static Expression<Func<Book, bool>> IsScienceFiction()
{
return book => book.Type == BookType.ScienceFiction;
}
public static Expression<Func<Book, bool>> PublishedBefore(DateTime date)
{
return book => book.PublishDate < date;
}
}
Bu spesifikasyonlar repository sorgularında kullanılabilir:
C#
var scienceFictionBooks = await _bookRepository.GetListAsync(
BookSpecifications.IsScienceFiction()
);
Repository Pattern ve IRepository Interface’i
Repository Pattern (Depo Deseni), veri erişim mantığını domain katmanından soyutlar. Entity’lerin veri depolama mekanizmasından habersiz olmasını sağlar. ABP Framework, varsayılan olarak EF Core tabanlı bir repository implementasyonu sunar.
ABP’de IRepository<TEntity, TKey>
arayüzü temel repository operasyonlarını (CRUD, sorgulama) sağlar:
C#
using Volo.Abp.Domain.Repositories;
using MyProject.Books;
namespace MyProject.Domain.Repositories
{
public interface IBookRepository : IRepository<Book, Guid>
{
// Özel sorgular buraya eklenebilir
Task<Book> FindByNameAsync(string name);
Task<List<Book>> GetBooksByTypeAsync(BookType type);
}
}
ABP, IRepository
arayüzünü otomatik olarak implemente eder, bu nedenle çoğu durumda özel bir implementasyon yazmanıza gerek kalmaz. Ancak, daha karmaşık veya özel sorgulara ihtiyacınız varsa, kendi repository implementasyonunuzu yazabilirsiniz.
C#
// BookRepository implementasyonu (TodoApp.EntityFrameworkCore projesinde)
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Volo.Abp.Domain.Repositories.EntityFrameworkCore;
using Volo.Abp.EntityFrameworkCore;
using MyProject.Books;
namespace MyProject.EntityFrameworkCore.Repositories
{
public class EfCoreBookRepository : EfCoreRepository<MyProjectDbContext, Book, Guid>, IBookRepository
{
public EfCoreBookRepository(IDbContextProvider<MyProjectDbContext> dbContextProvider)
: base(dbContextProvider)
{
}
public async Task<Book> FindByNameAsync(string name)
{
return await GetQueryable()
.Where(b => b.Name == name)
.FirstOrDefaultAsync();
}
public async Task<List<Book>> GetBooksByTypeAsync(BookType type)
{
return await GetQueryable()
.Where(b => b.Type == type)
.ToListAsync();
}
}
}
Bu bölüm, domain katmanınızı güçlü ve sürdürülebilir bir şekilde inşa etmeniz için gerekli temel yapı taşlarını sunar.