Rent A Car Application
Github repository: https://github.com/SerkanTarakci/Rent-A-Car-Application
Engin Demiroğ’un eğitmenliğini üstlendiği Yazılım Geliştirici Yetiştirme Kampı’nda işlediğimiz konuları pekiştirme amacıyla verilen ödevler doğrultusunda geliştirdiğim bir .NET uygulaması. Bu uygulamada, kendim oluşturduğum RentACar veritabanında bulunan Brands, Cars, Colros, Customers, Rentals, Users tablolarındaki ü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 Autofac kütüphanesi, doğrulama işlemlerini kontrol etmek adına ise FluentValidation kütüphanesi kullanılmıştır. Uygulamada bulunan WebApi katmanı sayesinde ‘CRUD’ işlemlerini Postman rest client kullanarak test edebiliyoruz. Ayrıca AOP yapısı da uygulamada mevcuttur. Kısaca katmanları tanıyacak olursak:
Entites Layer
Projede herhangi bir custom mapping yapmadığımız için Entities katmanındaki sınıflarımızda yer alan propertyleri veritabanındaki gibi yazdık.
Örnek Car sınıfı:
public class Car : IEntity
{
public int Id { get; set; }
public int BrandId { get; set; }
public int ColorId { get; set; }
public string ModelYear { get; set; }
public decimal DailyPrice { get; set; }
public string Description { get; set; }
}
Entity içindeki classlarımızı 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.
Cars tablosu:
Data Access Layer
DataAccess katmanında bulunan RentACarContext isimli classımızda entitylerimizi, veritabanındaki ilgili tablolara bağlanacak şekilde programa tanıttık.
public class RentACarContext : DbContext
{
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlServer(@"Server=(localdb)\MSSQLLocalDB;Database=RentACar;Trusted_Connection=true ");
}public DbSet<Brand> Brands { get; set; }
public DbSet<Car> Cars { get; set; }
public DbSet<Color> Colors { get; set; }
public DbSet<Customer> Customers { get; set; }
public DbSet<User> Users { get; set; }
public DbSet<Rental> Rentals { get; set; }
}
Burada CRUD operasyonları (Create, Read, Update, Delete diye bilinen ve projemizde Add, Get, Update, Delete operasyonlarına karşılık gelen operasyonlar) Tüm entity sınıflarında ortak olduğu için Core katmanındaki DataAccess içerisinde IEntityRepository adında bir generic arayüz yaratıp bu arayüzden her bir entity için arayüzlerimizi oluşturduk. (Ör: IBrandDal, ICarDal…vb)
public interface IEntityRepository<T> where T:class, IEntity, new() // <T> -> bana çalışacağım tipi söyle
{
//ama bu T yerine her şeyi yazmamamız lazım. Veritabanı işlemleri yapacağımız için sadece Entity-Concretedeki classlar olabilmeli.
//generic constraint uygulayacağız.Yukarıdaki where ile kısıtladık. Sadece referans tip olacak ve IEntity türünde olacak dedik.
//new - newlenebilir olmalı demek.
List<T> GetAll(Expression<Func<T, bool>> filter=null); //burası direk filtreleyerek işlem yapabilmemize yarıyo. (ProductManager'de mesela)
T Get(Expression<Func<T, bool>> filter);//zorunlu
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.
ICarDal.cs:
public interface ICarDal : IEntityRepository<Car>
{
List<CarDetailDto> GetCarDetails(Expression<Func<Car, bool>> filter = null);
}
Burada ICarDal sınıfı IEntityRepositorydeki diğer metotları inherit ediyor. Kendisinin de CarDetailDto türünde liste döndüren GetCarDetails isminde bir metodu bulunmakta.
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. Entity sınıflarımız 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. Bunu Core katmanındaki DataAccess klasörü içerisinde EntityFramework adında bir klasör oluşturup onun içinde yapıyoruz.
public class EfEntityRepositoryBase<TEntity, TContext> : IEntityRepository<TEntity>
where TEntity : class, IEntity, new()
where TContext : DbContext, new()
{
public List<TEntity> GetAll(Expression<Func<TEntity, bool>> filter = null)
{
using (TContext context = new TContext())
{
return filter == null ? context.Set<TEntity>().ToList() : context.Set<TEntity>().Where(filter).ToList();
//filtre varsa ilgili filtreyi getir yoksa hepsini getir.
}
}
}
public void Add(TEntity entity)
{
//IDisposable pattern. Objenin işi bitince direkt hafızadan kaldırmaya yarıyor garbage collectoru beklemeden.
using (TContext context = new TContext())
{
var addedEntity = context.Entry(entity);
addedEntity.State = EntityState.Added;
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 EfCarDal sınıfımızı ve diğer sınıflarımızı az önce oluşturduğumuz generic sınıftan inherit edebiliriz.
EfCarDal sınıfımızdaki GetCarDetails metodumuzda araç bilgilerini listelerken marka ve renk bilgileriyle birlikte listeliyoruz. Bunu yapabilmek için Cars, Colors ve Brands tabloları arasında bağlantı kurmamız gerekir. Bunu da şu şekilde yapabiliriz:
public class EfCarDal : EfEntityRepositoryBase<Car, RentACarContext>, ICarDal
{
public List<CarDetailDto> GetCarDetails(Expression<Func<Car, bool>> filter = null)
{
using (RentACarContext context = new RentACarContext())
{
var result = from c in filter == null ? context.Cars : context.Cars.Where(filter)
join co in context.Colors
on c.ColorId equals co.ColorId
join b in context.Brands
on c.BrandId equals b.BrandId
select new CarDetailDto
{
CarId = c.Id,
BrandName = b.BrandName,
ColorName = co.ColorName,
DailyPrice = c.DailyPrice,
Description = c.Description,
ModelYear = c.ModelYear
};
return result.ToList();
}
}
}
Result Yapılandırması
Normalde bir metot bir türde değer döndürebilir. Biz daha fazla sonuç döndürmek istediğimiz için kendi yapılandırmamızı kurduk. Core katmanında Utilities klasörü oluşturuyoruz ve onun içinde oluşturduğumuz Result klasöründe Result yapımızı oluşturuyoruz. IResult ve IDataResult arayüzlerimizi oluşturduk. IResult arayüzü bir işlemin başarılı olup olmadığını ve varsa işlemle ilgili bir mesajı döndürüyor. IDataResult arayüzü de IResult arayüzünden implement ediliyor ve ek olarak data döndürüyor. Burada oluşturduğumuz yapıyı daha sonra Business katmanında Service ve Manager sınıflarında kullanacağız.
IResult.cs:
public interface IResult
{
bool Success { get; }
string Message { get; }
}
IDataResult.cs:
public interface IDataResult<T> : IResult
{
T Data { get; }
}
Result.cs:
public class Result : IResult
{
public Result(bool success, string message) : this(success) //*
{
Message = message;
}
public Result(bool success)
{
Success = success;
}
public bool Success { get; } //**
public string Message { get; }
}
Bu sınıfı şöyle açıklayabiliriz:
*: Resultun tek parametreli constructorunu da çalıştırır. burada this Result classını temsil ediyor. this(success) : Result’un tek parametreli(success) constructoru demek oluyor.
Eğer constructora sadece success parametresi verilirse alttaki tek parametreli constructor çalışacaktır. Hem success hem mesaj parametresi verilirse üstteki çalışır ama altaki successi de çalıştırır.
- *: getterlar read onlydir ve bunlar constructorda set edilebilir. Bu yüzden setter koymadık. Böylece sadece constructorda set edilir.
DataResult.cs:
public class DataResult<T> : Result, IDataResult<T>
{
public DataResult(T data, bool success, string message) : base(success, message)
{
Data = data;
}
public DataResult(T data, bool success) : base(success)
{
Data = data;
}
public T Data { get; }
}
ConsoleUI Layer
Output:
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 ICarService
{
IDataResult<List<Car>> GetAll();
IDataResult<Car> Get(int id); //idye göre arıyoruz tek değer döneceği için Car türü verdik direkt liste vermedik.
IDataResult<List<Car>> GetCarsByBrandId(int brandId);
IDataResult<List<Car>> GetCarsByColorId(int colorId); IDataResult<List<CarDetailDto>> GetCarDetails(Expression<Func<Car, bool>> filter = null); IResult Add(Car car);
IResult Update(Car car);
IResult Delete(Car car);}
Dikkat ederseniz buradaki dönüş türleri IDataResult ve IResult. Bunları hatırlarsanız Core katmanında tanımlamıştık. Bir takım doğrulamalar ve iş kuralları sonucunda kullanıcıya data ve mesaj döndürme işlemini Business katmanında yaptığımız için bu dönüş tiplerini burada bu şekilde kullanıyoruz.
Concrete dosyamızın içinde de arayüzden türettiğimiz sınıflarımızın içerisindeki metotları doldurduk.
public class CarManager : ICarService
{
//Bir iş sınıfı başka sınıfları newlemesin. Bunun için injection yapıyoruz.
ICarDal _carDal;
public CarManager(ICarDal carDal)
{
_carDal = carDal;
}
public IDataResult<List<Car>> GetAll()
{ return new SuccessDataResult<List<Car>>(_carDal.GetAll()); }
public IDataResult<Car> Get(int id)
{
return new SuccessDataResult<Car>(_carDal.Get(c => c.Id == id));
}
public IDataResult<List<Car>> GetCarsByBrandId(int brandId)
{
return new SuccessDataResult<List<Car>>(_carDal.GetAll(c => c.BrandId == brandId));
}
public IDataResult<List<Car>> GetCarsByColorId(int colorId)
{
return new SuccessDataResult<List<Car>>(_carDal.GetAll(c => c.ColorId == colorId));}
[ValidationAspect(typeof(CarValidator))]*
public IResult Add(Car car)
{
//business codes
//validation
_carDal.Add(car);
return new SuccessResult(Messages.CarAdded);
}
public IResult Update(Car car)
{
_carDal.Update(car);
return new SuccessResult(Messages.CarUpdated);
}
public IResult Delete(Car car)
{
_carDal.Update(car);
return new SuccessResult(Messages.CarDeleted);
}
public IDataResult<List<CarDetailDto>> GetCarDetails(Expression<Func<Car, bool>> filter = null)
{
return new SuccessDataResult<List<CarDetailDto>>(_carDal.GetCarDetails(filter));
}
//bu sayede ne in memory ne entity ismi geçecek
}
*: AOP yani aspect oriented programming ile ilgili yazının ilerleyen kısmında bilgi vereceğim.
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 girilen ürün doğrulamayı geçerse veri erişim katmanındaki Add metodu çağrılacak. Ayrıca duruma göre ilgili mesaj da burada gösterilebilir.
Mesajlar için Business katmanında Constants adında bir klasör oluşturup içerisinde Messages.cs sınıfını yarattık.
public static class Messages
{
public static string BrandAdded = "Brand added";
public static string BrandUpdated = "Brand updated";
public static string BrandDeleted = "Brand deleted";
public static string BrandListed = "Brand listed"; public static string ColorAdded = "Color added";
public static string ColorUpdated = "Color updated";
public static string ColorDeleted = "Color deleted";
public static string ColorListed = "Color listed"; ...
}
Doğrulama işlemini FluentValidation kütüphanesini kullanarak yapıyoruz.
Bu işlemler iş katmanında olacağı için Business katmanı içerisinde ValidationRules adında bir klasör ve bunun içerisine de FluentValidation adlı bir klasör oluşturup orada entitylerimiz için validator.cs sınıfları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.
class CarValidator : AbstractValidator<Car>
{
public CarValidator()
{
RuleFor(c => c.DailyPrice).NotEmpty();
RuleFor(c => c.Description).MinimumLength(2);
RuleFor(c => c.DailyPrice).GreaterThanOrEqualTo(150).When(c => c.BrandId == 1);
//RuleFor(c => c.Description).Must(StartWithA);
}//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. Core katmanında CrossCuttingConcerns adında bir klasör oluşturup içerisinde ValidationTool.cs sınıfını yarattık.
ValidationTool.cs
public static void Validate(IValidator validator, object entity)
{ var context = new ValidationContext<object>(entity);
//bir entity için doğrulama yapacağım. Çalışacağım tip de parametremden gelen entity. //üstteki contexti nasıl doğrulayacağım? IValidator türünden validatoru kullanarak. (bizim car validator)
var result = validator.Validate(context);
//üstteki yorumda yazdığımız işlemi yapıyoruz yani parametre ile verdiğimiz car objesini
//carvalidator ile doğruluyoruz ve sonucu result diye bir değişkene atıyoruz. if (!result.IsValid)
{
throw new ValidationException(result.Errors);
}
}
Burada Validate metoduna IValidator türünde bir validator ve işlemleri üzerinde uygulayabilmesi adına obje türünde bir entity parametresi verdik. Yukarıda oluşturduğumuz CarValidator, IValidator türünde bir sınıf. Kullanımını görmek için yukarıdaki CarManager.cs sınıfındaki Add metoduna bakabilirsiniz. Orada CarValidator’un yanına bir car objesi veriyoruz ve verdiğimiz car objesi için CarValidator sınıfındaki kurallar kontrol ediliyor. Herhangi bir hata yoksa araba ekleme işlemi yapılabiliyor.
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 Autofac kullanacağız. İlerleyen zamanlarda başka bir servis kullanabiliriz. Bu yüzden Autofac adında ayrı bir klasör oluşturduk. İçerisinde AutofacBusinessModule.cs adında bir sınıf oluşturduk. Burada tanımlamalarımızı yapıyoruz.
AutofacBusinessModule.cs
public class AutofacBusinessModule : Module
{
protected override void Load(ContainerBuilder builder)
{
builder.RegisterType<BrandManager>().As<IBrandService>().SingleInstance();
builder.RegisterType<CarManager>().As<ICarService>().SingleInstance();
builder.RegisterType<ColorManager>().As<IColorService>().SingleInstance();
builder.RegisterType<CustomerManager>().As<ICustomerService>().SingleInstance();
builder.RegisterType<RentalManager>().As<IRentalService>().SingleInstance();
builder.RegisterType<UserManager>().As<IUserService>().SingleInstance();
builder.RegisterType<EfBrandDal>().As<IBrandDal>().SingleInstance();
builder.RegisterType<EfCarDal>().As<ICarDal>().SingleInstance();
builder.RegisterType<EfColorDal>().As<IColorDal>().SingleInstance();
builder.RegisterType<EfCustomerDal>().As<ICustomerDal>().SingleInstance();
builder.RegisterType<EfRentalDal>().As<IRentalDal>().SingleInstance();
builder.RegisterType<EfUserDal>().As<IUserDal>().SingleInstance(); var assembly = System.Reflection.Assembly.GetExecutingAssembly(); builder.RegisterAssemblyTypes(assembly)
.AsImplementedInterfaces()
.EnableInterfaceInterceptors(new ProxyGenerationOptions()
{
Selector = new AspectInterceptorSelector()
}).SingleInstance();
}
}
Burada “builder.RegisterType<BrandManager>().As<IBrandService>().SingleInstance();” komutu şu anlama geliyor, birisi consturctorda senden IBrandService türünde bir şey isterse ona somut bir BrandManager oluşturup döndür. Daha sonra WebApi katmanında Autofac ile çalıştığımızı belirtmemiz için tanımlama yapmamız gerekecek.
WebApi Layer
Uygulamamızı konsol haricinde Postman gibi bir rest client kullanarak internet tarayıcısı üzerinden de test edebiliriz. Bunu yapabilmemiz için Business katmanındaki tüm servislerin Api karşılığını yazıyoruz.
CarsController.cs:
public class CarsController : ControllerBase
{
ICarService _carService;public CarsController(ICarService carService)
{
_carService = carService;
}[HttpPost("add")]
public IActionResult Add(Car car)
{
var result = _carService.Add(car);
if (result.Success)
{
return Ok(result);
}
return BadRequest(result);
}[HttpPut("update")]
public IActionResult Update(Car car)
{
var result = _carService.Update(car);
if (result.Success)
{
return Ok(result);
}
return BadRequest(result);
}[HttpDelete("delete")]
public IActionResult Delete(Car car)
{
var result = _carService.Delete(car);
if (result.Success)
{
return Ok(result);
}
return BadRequest(result);
}[HttpGet("getall")]
public IActionResult GetAll()
{
var result = _carService.GetAll();
if (result.Success)
{
return Ok(result);
}
return BadRequest(result);
}[HttpGet("getbyid")]
public IActionResult GetById(int id)
{
var result = _carService.Get(id);
if (result.Success)
{
return Ok(result);
}
return BadRequest(result);
}[HttpGet("getallcardetails")]
public IActionResult GetAllCarDetails()
{
var result = _carService.GetCarDetails();
if (result.Success)
{
return Ok(result);
}
return BadRequest(result);
}[HttpGet("getcarbycolor")]
public IActionResult GetCarByColor(int id)
{var result = _carService.GetCarDetails(I => I.ColorId == id);
if (result.Success)
{
return Ok(result);
}
return BadRequest(result);
}[HttpGet("getcarbybrand")]
public IActionResult GetCarByBrand(int id)
{var result = _carService.GetCarDetails(I => I.BrandId == id);
if (result.Success)
{
return Ok(result);
}
return BadRequest(result);
}
}
WebApi’de Autofac yapılandırması:
WebApiye bizim autofac ile çalışmak istediğimizi bildirmemiz gerekiyor. Çünkü onun kendi IoC yapısı vardı biz onu kullanmak istemiyoruz. Web apideki program.csye geldik ve araya şu satırları ekledik:
.UseServiceProviderFactory(new AutofacServiceProviderFactory())
.ConfigureContainer<ContainerBuilder>(builder =>
{
builder.RegisterModule(new AutofacBusinessModule());
})
namespace WebApi
{
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.UseServiceProviderFactory(new AutofacServiceProviderFactory())
.ConfigureContainer<ContainerBuilder>(builder =>
{
builder.RegisterModule(new AutofacBusinessModule());
})
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
}
}
Output:
AOP — Aspect Oriented Programming
AOP nedir kısaca bahsedelim. Örneğin uygulamanın başında, sonunda veya hata aldığında çalışmasını istediğimiz kodlar varsa onları AOP ile dizayn edebiliriz. Mesela hata aldığında bununla ilgili loglama yapılması gibi. Bunlara interceptors deniyor, kelime anlamı araya girmek. Loglama, validation gibi işlemlerin gerçekleşmesi için metotların üzerine attribute ekliyoruz. Bir metot çağrılırken program metodun üzerinde attribute olup olmadığını kontrol eder. Belli bir kurala uyan attribute varsa ilgili metotları çalıştırır.
Bu yapıyı oluşturmak için yine genel bir yapı olacağından dolayı Core katmanında Utilities dosyası içinde bir Interceptors dosyası oluşturduk. İçerisinde 3 adet sınıf yarattık. Bu sınıfları oluştururken Engin Demiroğ’un daha önceden oluşturduğu bir projenin kodlarından yararlandık.
Bu sınıf özetle çalışmasını istediğimiz metodun ne zaman çalışacağını belirliyor. Buradaki ilgili virtual metotları kendi yazacağımız bir Aspect sınıfında override ediyoruz.
Core katmanındaki Aspects klasörü içerisinde bulunan ValidationAspect sınıfını MethodInterception sınıfından implement ettik. Bu şu anlama geliyor ValidationAspect bir Method Interception yani bizim Aspectimiz. Metodun başında, sonunda veya hata aldığında çalışacak yapı.
Örnek CarManager.cs:
[ValidationAspect(typeof(CarValidator))]
public IResult Add(Car car)
{
//business codes
//validation _carDal.Add(car);
return new SuccessResult(Messages.CarAdded);
}
Burada add çağrılacağı zaman önce üzerinde herhangi bir attribute olup olmadığı kontrol edilir. Attribute olduğunu gören program tanımlamış olduğumuz validate kuralını Add metodu için çalıştırır.
Yararlanılan kaynaklar:
https://github.com/engindemirog/NetCoreBackend
https://www.kodlama.io/p/yazilim-gelistirici-yetistirme-kampiGeliştirmenin yapıldığı ortam: Microsoft Visual Studio 2019