Merhaba, bu makalemde Visual Studio 2017 ve .NET Core ile Onion Architecture (Soğan Mimarisi) adı verilen çok katmanlı mimarinin nasıl kurulduğunu anlatacağım.
Öncelikle biraz katmanlı mimariden bahsedelim. Katmanlı mimari denilince İngilizce’de bu anlamda iki ifade bulunuyor: n-tired architecture ve multi-layered architecture. Türkçe’de bu İngilizce ifadelerin her ikisi de çok katmanlı mimari anlamında kullanılsa da aslında bu ifadelerin anlamları farklı. multi-layered architecture dediğimiz çok katmanlı mimari bir Visual Studio çözümününün içerisinde birden fazla proje olması durumudur. Her projeye layer yani katman denilir. n-tired ifadesi ise bu layerlerın farklı sunucularda yayınlanması durumudur. Her sunucudaki katmana tier denilir.
Biz bu makalemizde katmanlı mimari derken multi-layered architecture’ı kastediyoruz.
Çok katmanlı mimari desenleri çeşitlidir. Bunlardan Onion Arhcitecture’ı yani Soğan Mimarisini anlatacağım.
Soğan Mimarisi 4 katmandan oluşur:
1. Model katmanı
2. Data katmanı
3. Service katmanı
4. Presentation katmanı
Model Katmanı
Model katmanı entity sınıflarımızı içeren bir katmandır. Her entity ayrı bir class dosyasında yazılır. Enum’lar da aynı şekilde ayrı dosyalarda yer alır. Entity’lerin neredeyse tamamının miras aldığı BaseEntity adında bir sınıf bulunur. Sadece many-to-many ilişkileri kurmaya yarayan ara entityler BaseEntity’i miras almazlar. Entitylerde Data Annotation attribute’ları kullanılmaz. Bu attribute’lar yerine Data katmanında Builder class’ları ile fluent-api kodları yazılacaktır.
Öncelikle ilk olarak yapılması gereken kısım Files>New Project>Other Project Types>Visual Studio Solutions>Blank Solution şeklinde bir çözüm açılır. Daha sonra açılan pencereden Solution Explorer penceresindeki projeye sağ tıklayıp Add>New Project>.NET Core>Class Library(.Net Core)şeklinde bir sınıf kütüphanesi ekleyip OnionArchitectureSample.Model şeklinde isim verilir. Daha sonra OnionArchitectureSample.Model e sağ tıklayıp Add>Class yapılarak BaseEntity şeklinde bir sınıf eklenir. BaseEntity sınıfının kodları aşağıdaki gibi yazılabilir.
public class BaseEntity { public long Id { get; set; } public DateTime AddedDate { get; set; } public DateTime ModifiedDate { get; set; } public string AddedBy { get; set; } public string ModifiedBy { get; set; } }
Daha sonra tekrar aynı şekilde Category sınıfı eklenir ve kodları aşağıdaki gibi yazılabilir.
public class Category:BaseEntity { public string Name { get; set; } public virtual ICollection<Product> Products { get; set; } }
Aynı şekilde Product şeklinde bir sınıf daha eklenip kodları aşağıdaki gibi yazılabilir.
public class Product:BaseEntity { public string Title { get; set; } public string Description { get; set; } public decimal Price { get; set; } public long CategoryId { get; set; } public Category Category { get; set; } }
Data Katmanı
Data katmanında model katmanında tanımlanan entitylerin veritabanı işlemlerini yapmaya yarayan repository’ler bulunur. Aynı zamanda projenin kalbi olan Unit Of Work, Generic Respository sınıf ve interface’leri de bu katmanda bulunur. Her repository için aynı .cs uzantılı dosyada hem interface hem de class tanımlaması yapılacaktır ve repository’ler eager loading yapabilmek için Entity Framework’teki Include metotlarını da destekleyecektir.
Data katmanında aynı zamanda DbContext’imiz ve Seed extension metodumuz da bulunmaktadır. DbContext’in çalışabilmesi için bu katmanı Entity Framework Core nuget pakedini yüklememiz gerekecektir.
Şimdi, OnionArchitectureSample.Model i eklediğimiz gibi yeni bir proje daha ekleyip OnionArchitectureSample.Data ismini verelim. OnionArchitectureSample.Data ya sağ tıklayıp New Folder kısmından yeni klasör ekleyip ismine Builders verilir. Builders klasörüne sağ tıklayıp sınıf ekleyelim ve ismine de CategoryBuilder verebiliriz. Açılan CategoryBuilder sınıfının yapıcı metodu aşağıdaki gibidir:
public CategoryBuilder(EntityTypeBuilder<Category> entityBuilder) { entityBuilder.HasKey(p => p.Id); entityBuilder.Property(p => p.Name).IsRequired().HasMaxLength(200); }
Aynı şekilde ProductBuilder adından bir sınıf daha eklenir. ProductBuilder sınıfının kod kısmı aşağıdaki gibidir:
public ProductBuilder(EntityTypeBuilder<Product> entityBuilder) { entityBuilder.HasKey(p => p.Id); entityBuilder.Property(p => p.Title).IsRequired().HasMaxLength(200); entityBuilder.HasOne(p => p.Category).WithMany(c => c.Products) .HasForeignKey(p => p.CategoryId); }
Builders klasörünü eklediğimiz gibi Infrastructure isminde bir klasör daha ekleyelim. Infrastructure klasörüne sağ tıklayıp IRepository isminde bir interface ekleyelim. IRepository interface’inin kod kısmı aşağıdaki gibidir:
public interface IRepository<T> where T : class { void Add(T entity); void Update(T entity); void Delete(T entity); void Delete(Expression<Func<T, bool>> where); T GetById(long id, params string[] navigations); T Get(Expression<Func<T, bool>> where, params string[] navigations); IEnumerable<T> GetAll(params string[] navigations); IEnumerable<T> GetMany(Expression<Func<T, bool>> where, params string[] navigations); }
Aynı şekilde Infrastructure klasörünün altına IUnitOfWork adında bir interface daha ekleyelim. IUnitOfWork interface’inin kod kısmı aşağıdaki gibidir.
public interface IUnitOfWork { void Commit(); }
Aynı şekilde RepositoryBase isminde bir sınıf eklenir. RepositoryBase sınıfının kod kısmı aşağıdaki gibidir.
public abstract class RepositoryBase<T> where T : BaseEntity { #region Properties private ApplicationDbContext dataContext; private readonly DbSet<T> dbSet; protected ApplicationDbContext DbContext { get { return dataContext; } } #endregion protected RepositoryBase(ApplicationDbContext dbContext) { dataContext = dbContext; dbSet = DbContext.Set<T>(); } #region Implementation public virtual void Add(T entity) { dbSet.Add(entity); } public virtual void Update(T entity) { dbSet.Attach(entity); dataContext.Entry(entity).State = EntityState.Modified; } public virtual void Delete(T entity) { dbSet.Remove(entity); } public virtual void Delete(Expression<Func<T, bool>> where) { IEnumerable<T> objects = dbSet.Where<T>(where).AsEnumerable(); foreach (T obj in objects) dbSet.Remove(obj); } public virtual T GetById(long id, params string[] navigations) { var set = dbSet.AsQueryable(); foreach (string nav in navigations) set = set.Include(nav); return set.FirstOrDefault(f => f.Id == id); } public virtual IEnumerable<T> GetAll(params string[] navigations) { var set = dbSet.AsQueryable(); foreach (string nav in navigations) set = set.Include(nav); return set.AsEnumerable(); } public virtual IEnumerable<T> GetMany(Expression<Func<T, bool>> where, params string[] navigations) { var set = dbSet.AsQueryable(); foreach (string nav in navigations) set = set.Include(nav); return set.Where(where).AsEnumerable(); } public T Get(Expression<Func<T, bool>> where, params string[] navigations) { var set = dbSet.AsQueryable(); foreach (string nav in navigations) set = set.Include(nav); return set.Where(where).FirstOrDefault<T>(); } #endregion }
Aynı şekilde UnitOfWork isminde bir sınıf daha eklenir. UnitOfWork sınıfının kod kısmı aşağıdaki gibidir.
public class UnitOfWork : IUnitOfWork { private ApplicationDbContext dbContext; public UnitOfWork(ApplicationDbContext dbContext) { this.dbContext = dbContext; } public ApplicationDbContext DbContext { get { return dbContext; } } public void Commit() { try { DbContext.SaveChanges(); } catch (Exception ex) { throw ex; } } }
Builders ve Infrastructure klasörlerini eklediğimiz gibi Repositories isminde bir klasör daha ekleyelim. Repositories klasörüne sağ tıklayıp CategoryRepository isminde bir sınıf ekleyelim. CategoryRepository sınıfının kod kısmı aşağıdaki gibidir.
public class CategoryRepository : RepositoryBase<Category>, ICategoryRepository { public CategoryRepository(ApplicationDbContext dbContext) : base(dbContext) { } public Category GetCategoryByName(string categoryName) { var category = this.DbContext.Categories.Where(c => c.Name == categoryName).FirstOrDefault(); return category; } } public interface ICategoryRepository : IRepository<Category> { Category GetCategoryByName(string categoryName); }
OnionArchitectureSample.Data projesine sağ tıklayıp ApplicationDbContext isminde bir sınıf ekleyelim. ApplicationDbContext sınıfının kodları aşağıdaki gibidir.
public class ApplicationDbContext:DbContext { public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) :base(options) { } public ApplicationDbContext() { } public DbSet<Product> Products { get; set; } public DbSet<Category> Categories { get; set; } protected override void OnModelCreating(ModelBuilder builder) { base.OnModelCreating(builder); new ProductBuilder(builder.Entity<Product>()); new CategoryBuilder(builder.Entity<Category>()); } }
OnionArchitectureSample.Data projesine sağ tıklayıp ApplicationDbContextSeed isminde bir sınıf ekleyelim. ApplicationDbContextSeed sınıfının kodları aşağıdaki gibidir.
public static class ApplicationDbContextSeed { public static void Seed(this ApplicationDbContext context) { context.Database.Migrate(); if(context.Categories.Any()) { return; } AddCategories(context); AddProducts(context); } private static void AddCategories(ApplicationDbContext context) { context.AddRange( new Category { Name = "Telefon" }, new Category { Name = "Tablet" }, new Category { Name = "Bilgisayar" }, new Category { Name = "Televizyon" } ); context.SaveChanges(); } private static void AddProducts(ApplicationDbContext context) { context.AddRange( new Product { Title = "iPhone", Price = 4000, CategoryId = 1 } ); context.SaveChanges(); } }
Service Katmanı
Data katmanındaki repository’lerin bir ya da bir kaçını kullanan bussiness logic katmanımız olan service katmanında ProductService ya da CategoryService gibi class’lar tanımlanır. Bu serviceler genellikle kayıt ekleme, güncelleme, arama ve silme işlemleri yapmak için repository’leri kullanır ve değişiklikleri veritabanına aynı transaction ve context ile uygulayabilmek için unit of work’tan yararlanır.
Beraber yapalım…
OnionArchitectureSample.Model i eklediğimiz gibi yeni bir proje daha ekleyip OnionArchitectureSample.Service ismini verelim. OnionArchitectureSample.Service ya sağ tıklayıp yeni bir sınıf ekleyelim ve ismine CategoryService diyelim. CategoryService dosyasının içine aşağıdaki interface ve sınıfı ekleyelim:
public interface ICategoryService { Category GetCategory(long id); IEnumerable<Category> GetCategories(); void CreateCategory(Category category); void UpdateCategory(Category category); void DeleteCategory(long id); void SaveCategory(); } public class CategoryService :ICategoryService { private readonly ICategoryRepository categoryRepository; private readonly IUnitOfWork unitOfWork; public CategoryService(ICategoryRepository categoryRepository, IUnitOfWork unitOfWork) { this.categoryRepository = categoryRepository; this.unitOfWork = unitOfWork; } #region ICategoryService Members public IEnumerable<Category> GetCategories() { var categories = categoryRepository.GetAll(); return categories; } public Category GetCategory(long id) { var category = categoryRepository.GetById(id); return category; } public void CreateCategory(Category category) { categoryRepository.Add(category); } public void UpdateCategory(Category category) { categoryRepository.Update(category); } public void DeleteCategory(long id) { categoryRepository.Delete(pc => pc.Id == id); } public void SaveCategory() { unitOfWork.Commit(); } #endregion }
Presentation Katmanı
MVC projemiz bu katmandadır. Startup sınıfında .NET Core ile birlikte gelen gömülü dependency injection özelliğinden yararlanarak DbContext ve Unit Of Work scoped service olarak eklenir. Repository ve Service’ler ise Transient service olarak eklenir.
AutoMapper pakedi yüklenerek Model->ViewModel ve ViewModel-> Model dönüşümleri konfigüre edilir.
MVC proje oluşturulurken .NET Core’un Empty yerine Web Application template’nin kullanılmasını tavsiye ederim. Böylece MVC dizin yapısı hazır olarak oluşacaktır ve işlemlerimizi daha hızlı tamamlayabiliriz.
Not: Bu makale öğrencim Ömer Örenç’in katkılarıyla hazırlanmıştır.