Layered App Demo
Github repository: https://github.com/SerkanTarakci/LayeredAppDemo
Katmanlı mimarileri öğrenmek için geliştirdiğim bir Windows Forms uygulaması. Bu uygulamada, Northwind veritabanında bulunan Products ve Categories tablosundaki ürünler ile ilgili Entity Framework yardımıyla Ekleme, Listeleme, Güncelleme, Silme işlemleri yapılabilmekte. Uygulamada ayrıca bağımlı nesneleri kontrol etmek adına Ninject kütüphanesi, doğrulama işlemlerini kontrol etmek adına ise FluentValidation kütüphanesi kullanılmıştır. Kısaca katmanları tanıyacak olursak:
Entities Layer
Projede herhangi bir custom mapping yapmadığımız için Northwind.Entities katmanındaki sınıflarımızda yer alan propertyleri veritabanındaki gibi yazdık.
Örnek Product sınıfı:
public class Product : IEntity
{
public int ProductId { get; set; }
public string ProductName { get; set; }
public int CategoryId { get; set; }
public decimal UnitPrice { get; set; }
public string QuantityPerUnit { get; set; }
public Int16 UnitsInStock { get; set; }
}
Burada Product ve Category classlarını IEntity diye bir arayüzden türettik. Bunun sebebine özetle, bu classların veritabanında bir tabloya karşılık geldiğini belirtmek istememiz, ileride veri erişimi katmanında generic metotlar oluştururken bu metotlarda kısıtlama yapacak isek işimizi kolaylaştıracak olması veya ileride yapabileceğimiz daha farklı değişikliklere daha kolay bir şekilde uyum sağlayabilmek diyebiliriz.
Products tablosu:
Data Access Layer
Northwind.DataAccess katmanında bulunan NorthwindContext isimli classımızda Product entitymizi, veritabanındaki Products tablosuna bağlanacak şekilde programa tanıttık.
public class NortwindContext : DbContext
{
public DbSet<Product> Products { get; set; }
public DbSet<Category> Categories { get; set; }
}
Burada CRUD operasyonları (Create, Read, Update, Delete diye bilinen ve projemizde Add, Get, Update, Delete operasyonlarına karşılık gelen operasyonlar) Product ve Category sınıflarında ortak olduğu için Abstract klasöründe IEntityRepository adında bir generic arayüz yaratıp bu arayüzden ICategoryDal ve IProdoctDal arayüzlerini oluşturduk.
public interface IEntityRepository<T> where T: class, IEntity, new() {
List<T> GetAll(Expression<Func<T,bool>> filter = null);
T Get(Expression<Func<T, bool>> filter);
void Add(T entity);
void Update(T entity);
void Delete(T entity);
}
Burada where sayesinde bu generic arayüze kısıtlama getirebiliyoruz. Bu arayüzü implement edecek nesne referans tip olmalı, IEntity türünde olmalı ve “new”lenebilmeli.
public interface IProductDal : IEntityRepository<Product>
{
}
Veri erişimi katmanımızdaki Concrete klasöründe de daha önce Abstract klasöründe oluşturduğumuz arayüzleri kullanarak bu arayüzdeki metotları ilgili veri kaynağı için dolduracağız. Product ve Category sınıfları için metotların çoğu yine ortak olacağı için az önce yaptığımız gibi EfEntityRepositoryBase generic sınıfımızı oluşturuyoruz.
EfEntityRepositoryBase.cs:
public class EfEntityRepositoryBase<TEntity, TContext> : IEntityRepository<TEntity>
where TEntity:class, IEntity, new()
where TContext:DbContext,new()
{
public void Add(TEntity entity)
{
using (TContext context = new TContext())
{
var addedEntity = context.Entry(entity);
addedEntity.State = EntityState.Added;
context.SaveChanges();
}
} public void Delete(TEntity entity)
{
using (TContext context = new TContext())
{
var deletedEntity = context.Entry(entity);
deletedEntity.State = EntityState.Deleted;
context.SaveChanges();
}
} ...
Burada generic sınıfımıza parametre olarak entitiy türü ve veritabanı contexti girilmesini istiyoruz. Yine where keywordü ile sınıfımıza kısıtlamalar ekliyoruz. Daha sonra bu genericleri kullanarak metotların içini dolduruyoruz. Sonrasında artık Entity Framework için ProductDal sınıfımızı az önce oluşturduğumuz generic sınıftan inherit edebiliriz.
public class EfProductDal: EfEntityRepositoryBase<Product,NortwindContext>, IProductDal
{
}
Business Layer
Burada diğer katmanlarımızda da olduğu gibi Abstract ve Concrete dosyaları oluşturduk. Abstract dosyasında arayüzlerimizi tanımladık.
public interface IProductService
{
List<Product> GetAll();
List<Product> GetProductsByCategory(int categoryId);
List<Product> GetProductsByProductName(string productName);
void Add(Product product);
void Update(Product product);
void Delete(Product product);
}
Concrete dosyamızın içinde de arayüzden türettiğimiz sınıflarımızın içerisindeki metotları doldurduk.
public class ProductManager : IProductService
{
public IProductDal _productDal;public ProductManager(IProductDal productDal)
{
_productDal = productDal;
}public void Add(Product product)
{
ValidationTool.Validate(new ProductValidator(), product);
_productDal.Add(product);
}...
Burada iş kodlarımız bulunuyor (doğrulama gibi) ve bu iş kodlarının sonucuna göre veri erişim katmanındaki ilgili metot çağrılıyor. Örneğin Add metodu için eğer forma girilen ürün doğrulamayı geçerse veri erişim katmanındaki Add metodu çağrılacak. Doğrulama işlemini FluentValidation kütüphanesini kullanarak yapıyoruz.
Bu işlemler iş katmanında olacağı için Northwind.Business katmanı içerisinde ValidationRules adında bir klasör ve bunun içerisine de FluentValidation adlı bir klasör oluşturup orada Product entitymiz için ProductValidator.cs sınıfını yarattık. Bu şekilde klasör oluşturmamızın sebebi ise SOLID prensiplerinin “O“ harfine karşılık gelen Open-Closed prensibine uymak. Yani ileride FluentValidation’dan başka bir doğrulama aracı kullanabiliriz ve programa ekleyeceğimiz bu yeni doğrulama sistemi mevcut sistemde değişiklik yapmaya sebep olmamalıdır.
public class ProductValidator : AbstractValidator<Product>
{
public ProductValidator()
{
RuleFor(p => p.ProductName).NotEmpty();
RuleFor(p => p.CategoryId).NotEmpty().WithMessage("CategoryId cannot be empty"); //mesaj da basabiliyoruz
RuleFor(p => p.UnitPrice).NotEmpty();
RuleFor(p => p.QuantityPerUnit).NotEmpty();
RuleFor(p => p.UnitsInStock).NotEmpty(); RuleFor(p => p.UnitPrice).GreaterThan(0);
RuleFor(p => p.UnitsInStock).GreaterThanOrEqualTo((short)0);
RuleFor(p => p.UnitPrice).GreaterThan(10).When(p => p.CategoryId==2); RuleFor(p => p.ProductName).Must(StartWithA).WithMessage("Product name must start with A");
}private bool StartWithA(string arg)
{
return arg.StartsWith("A");
}
}
Doğrulama işlemlerini farklı metotlarda, farklı sayfalarda da yapabiliriz. Bu yüzden onu da generic bir şekilde yazmayı tercih edebiliriz. Utilities adında bir klasör oluşturup içerisinde ValidationTool.cs sınıfını yarattık.
public static class ValidationTool
{
public static void Validate(IValidator validator, object entity)
{
var result = validator.Validate((IValidationContext)entity);
if (result.Errors.Count > 0)
{
throw new ValidationException(result.Errors);
}}
}
Burada Validate metoduna IValidator türünde bir validator ve obje türünde bir entity parametresi verdik. Yukarıda oluşturduğumuz ProductValidator, IValidator türünde bir sınıf. Kullanımını görmek için yukarıdaki ProductManager.cs sınıfındaki Add metoduna bakabilirsiniz.
Son olarak, SOLID’in “D” harfi, yani Dependency Inversion prensibine uymak için projemizde IoC Container kullanacağız. Bunun için DependencyResolvers adında bir klasör oluşturduk. Dependency resolver olarak uygulamamızda Ninject kullanacağız. İlerleyen zamanlarda başka bir servis kullanabiliriz. Bu yüzden Ninject adında ayrı bir klasör oluşturduk. İçerisinde BusinessModule.cs adında bir sınıf oluşturduk. Burada tanımlamalarımızı yapıyoruz.
public class BusinessModule : NinjectModule
{
public override void Load()
{
Bind<IProductService>().To<ProductManager>().InSingletonScope();
Bind<IProductDal>().To<EfProductDal>().InSingletonScope();
Bind<ICategoryService>().To<CategoryManager>().InSingletonScope();
Bind<ICategoryDal>().To<EfCategoryDal>().InSingletonScope();
}
}
Burada “Bind<IProductService>().To<ProductManager>().InSingletonScope();” komutu şu anlama geliyor, birisi consturctorda senden IProductService türünde bir şey isterse ona somut bir ProductManager oluşturup döndür.
Windows Forms uygulamalarında parametreli constructor gönderemediğimiz için bizim bu işlemi constructor kullanmadan yapmamız gerekiyor. Bunun için aynı klasörde InstanceFactory.cs adında bir sınıf oluşturduk.
InstanceFactory.cs:
public class InstanceFactory
{
public static T GetInstance<T>()
{
var kernel = new StandardKernel(new BusinessModule());
return kernel.Get<T>();
}
}
Daha sonra bunu Form1.cs’nin yapıcı metodunda şu şekilde kullandık:
public Form1()
{
InitializeComponent();
_productService = InstanceFactory.GetInstance<IProductService>();
_categoryService = InstanceFactory.GetInstance<ICategoryService>();
}
Görüldüğü üzere burada sınıflar arasındaki bağımlılık azalmış oldu. Yani bir sınıf diğer bir sınıfı direkt olarak “new”lemedi. Burada kodumuz BusinessModule.cs sınıfına gidiyor ve yazdığımız IProductService, ICategoryService parametrelerinin karşılığında bize ilgili nesneleri oluşturup döndürüyor.
UI Layer
Veritabanı ile bağlantının kurulabilmesi için de Northwind.WebformsUI katmanındaki App.config dosyasına connectionStringsi aşağıdaki şekilde ekledik:
<connectionStrings>
<add name ="NortwindContext" connectionString="data source=(localdb)\mssqllocaldb;initial catalog=northwind; integrated security=true" providerName="System.Data.SqlClient"/>
</connectionStrings>
Program.cs sınıfında program çalıştığı zaman Form1'i yüklemesi gerektiğini tanımladık:
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new Form1());
}
Form1.cs sınıfımızda da formda bulunan combobox, textbox gibi bileşenleri tanımladık. İlgili alanlarda gösterilmesi gereken verileri Business katmanı aracılığıyla veritabanından çektik ve arayüzdeki butonlara tıklandığı zaman çalışması gereken operasyonları burada tanımladık.
Output
Başlıca yararlanılan kaynak: https://www.btkakademi.gov.tr/
Geliştirmenin yapıldığı ortam: Microsoft Visual Studio 2019
Kullanılan veritabanı: https://github.com/Microsoft/sql-server-samples/tree/master/samples/databases/northwind-pubs