A few years ago I had a project, it was just a simple web site. It was an order and the occupation of the company was about import/export in Russia. I'm just allowed to make the uncompleted version of the project as open source! And it would be sufficient for grasping the whole! The project's architecture was something like DDD-lite, although it didn't need to pay such a complexity! You can get the full source from my Github account.
Model, Repository, Service and an MVC project as presentation layer are the projects of the solution. There is another project for keeping the resources as it was multilingual. The is an interface named IAggregateRoot in the Model layer which is just for flagging the model as an aggregate root. I have to mention again this project is not DDD and it neither doesn't have the complexities of DDD nor it needs those complexities. The IAggregateRoot has simply defined like below:
public interface IAggregateRoot { }
There is another interface in the Model layer named IRepository which is generic to get the model classes to implement in repository layer:
public interface IRepository<T> { IEnumerable<T> GetAll(); T FindBy(params Object[] keyValues); void Add(T entity); void Update(T entity); void Delete(T entity); void SaveChanges(); }
As an example I describe the most complex model in the project which has some dependency classes:
public class Category { public int Id { get; set; } public string Name { get; set; } public virtual ICollection<Product> Products { get; set; } } public class Brand { public int Id { get; set; } public string Name { get; set; } }
As you see, there are not AggregateRoot. (I've blogged about DDD and DDD-lite fundamental before if you are not familiar with) and about the Product model:
public class Product : IAggregateRoot { private readonly IList<Category> _category; private readonly IList<Brand> _brands; public Product() { } public Product(IList<Category> category) { _category = category; } public Product(IList<Brand> brands) { _brands = brands; } public int Id { get; set; } public string Name { get; set; } public decimal Price { get; set; } public string Description { get; set; } public DateTime CreationTime { get; set; } public string Picture { get; set; } public int BrandId { get; set; } public virtual Brand Brand { get; set; } public virtual ICollection<Category> Categories { get { return _category; } } #region Category public void AddCategory(Category category) { _category.Add(category); } #endregion #region public void AddBrand(Brand brand) { _brands.Add(brand); } #endregion }
As you can see I've added brand and category in Product Model. Now in the Repository layer, I have the Entity Framework DbContext:
public class StatosContext : DbContext { public StatosContext() { if (Database.Exists() && !Database.CompatibleWithModel(false)) Database.Delete(); if (!Database.Exists()) Database.Create(); } public StatosContext(string connectionString) : base(connectionString) { } protected override void OnModelCreating(DbModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder.Configurations.Add(new AccountMapping()); modelBuilder.Configurations.Add(new RoleMapping()); modelBuilder.Configurations.Add(new UserRoleMapping()); modelBuilder.Configurations.Add(new CategoryMapping()); modelBuilder.Configurations.Add(new ContactMapping()); modelBuilder.Configurations.Add(new ProductMapping()); modelBuilder.Configurations.Add(new StoreMapping()); modelBuilder.Configurations.Add(new ContentMapping()); modelBuilder.Configurations.Add(new BlogMapping()); modelBuilder.Configurations.Add(new MemberMapping()); modelBuilder.Configurations.Add(new BrandMapping()); modelBuilder.Configurations.Add(new LanguageMapping()); } public DbSet<Account> Account { get; set; } public DbSet<Role> Roles { get; set; } public DbSet<UserRole> UserRoles { get; set; } public DbSet<Category> Category { get; set; } public DbSet<Contact> Contact { get; set; } public DbSet<Product> Product { get; set; } public DbSet<Store> Store { get; set; } public DbSet<Content> Content { get; set; } public DbSet<Blog> Blog { get; set; } public DbSet<Member> Member { get; set; } public DbSet<Brand> Brand { get; set; } public DbSet<Language> Language { get; set; } }
And the base Repository class that we defined its interfaces in the Model layer:
public class Repository<T> : IRepository<T> where T : class, IAggregateRoot { private readonly DbSet<T> _entitySet; private readonly StatosContext _statosContext; public Repository(StatosContext statosContext) { _statosContext = statosContext; _entitySet = statosContext.Set<T>(); } public IEnumerable<T> GetAll() { try { return _entitySet; } catch (Exception exception) { // catch } return _entitySet; } public T FindBy(params Object[] keyValues) { return _entitySet.Find(keyValues); } public void Add(T entity) { _entitySet.Add(entity); } public void Update(T entity) { _entitySet.Attach(entity); _statosContext.Entry(entity).State=EntityState.Modified; } public void Delete(T entity) { var e = _statosContext.Entry(entity); if (e.State == EntityState.Detached) { _statosContext.Set<T>().Attach(entity); e = _statosContext.Entry(entity); } e.State=EntityState.Deleted; } public void SaveChanges() { _statosContext.SaveChanges(); } }
For mapping the Product aggregate members:
public class BrandMapping :EntityTypeConfiguration<Brand> { public BrandMapping() { ToTable("Brand"); Property(c => c.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity); Property(c => c.Name).IsOptional(); Property(c => c.Name).IsRequired().HasMaxLength(40); } } public class CategoryMapping : EntityTypeConfiguration<Category> { public CategoryMapping() { ToTable("Category"); HasKey(c => c.Id); Property(c => c.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity); Property(c => c.Name).IsRequired().HasMaxLength(50); } } public class ProductMapping : EntityTypeConfiguration<Product> { public ProductMapping() { ToTable("Product"); HasKey(p => p.Id); Property(p => p.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity); Property(p => p.Name).IsRequired(); Property(p => p.Price).IsOptional(); Property(p => p.Description).IsOptional(); Property(p => p.Description).IsOptional(); Property(p => p.CreationTime).IsRequired(); Property(p => p.Picture).IsOptional(); HasRequired(p => p.Brand); } }
And the Product aggregate repository I have written it like this:
public class ProductRepository : Repository<Product>, IProductRepository { private readonly StatosContext _statosContext; public ProductRepository(StatosContext statosContext) : base(statosContext) { _statosContext = statosContext; } #region Products /// <summary> /// Returns All Products By Category /// </summary> /// <param name="categoryId"></param> /// <returns></returns> public IEnumerable<Product> FindAllProductsByCategory(int categoryId) { var query = _statosContext.Product.Where(p => p.Categories.Any(c => c.Id == categoryId)).OrderByDescending(p => p.CreationTime); return query.ToList(); } /// <summary> /// Returns the recent products based on requested number /// </summary> /// <param name="number"></param> /// <returns></returns> public IEnumerable<Product> FindRecentProducts(int number) { return _statosContext.Product.OrderByDescending(p => p.CreationTime).Take(number).ToList(); } /// <summary> /// returns Products by brand /// </summary> /// <param name="brandId"></param> /// <returns></returns> public IEnumerable<Product> FindAllProductsByBrand(int brandId) { var query = _statosContext.Product.Where(p => p.Brand.Id == brandId).OrderByDescending(p => p.CreationTime); return query.ToList(); } #endregion #region Categories public IEnumerable<Category> FindAllCategories() { return _statosContext.Category.AsEnumerable(); } #endregion #region Brands public IEnumerable<Brand> FindAllBrands() { return _statosContext.Brand.AsEnumerable(); } #endregion }
Not that I didn't create a Repository for None-AggregateRoot classes! Now it turns to Service layer. Service which is a layer that ViewModels, Validations and Mappings live there. About the ViewModels:
public class BrandViewModel { public int BrandId { get; set; } [Required(ErrorMessage = "Name is required")] public string Name { get; set; } } public class CategoryViewModel { public int CategoryId { get; set; } [Required(ErrorMessage = "Name is required")] public string Name { get; set; } } public class ProductViewModel { public Guid ProductId { get; set; } public Guid CategoryId { get; set; } public Guid BrandId { get; set; } [Required(ErrorMessage="Product Name is Empty")] public string Name { get; set; } public decimal Price { get; set; } public string Description { get; set; } public Brand Brand { get; set; } public string Picture { get; set; } public Category Category { get; set; } public DateTime CreationTime { get; set; } }
And about the service interface:
public interface IProductService { #region Product IEnumerable<ProductViewModel> GetAllProducts(); void CreateProduct(ProductViewModel productViewModel); ProductViewModel GetProduct(int productId); void UpdateProduct(ProductViewModel productViewModel); void RemoveProduct(int productId); IEnumerable<ProductViewModel> GetAllProductsByCategory(int categoryId); IEnumerable<ProductViewModel> GetLatestProducts(); IEnumerable<ProductViewModel> GetAllProductstByBrand(int brandId); #endregion #region Brands void CreateBrand(BrandViewModel brandViewModel); IEnumerable<BrandViewModel> GetAllBrands(); void RemoveBrand(int brandId); #endregion #region Category void CreateCategory(CategoryViewModel categoryViewModel); IEnumerable<CategoryViewModel> GetAllCategories(); void RemoveCategory(int categoryId); #endregion }
And about the implementing of the interface there would be a class:
public class ProductService : IProductService { private readonly IProductRepository _productRepository; #region Products public ProductService(IProductRepository productRepository) { _productRepository = productRepository; } /// <summary> /// returns all products of the system /// </summary> /// <returns></returns> public IEnumerable<ProductViewModel> GetAllProducts() { var products = _productRepository.GetAll().OrderByDescending(p => p.CreationTime); var productsList = products.ConvertToProductViewList(); return productsList; } /// <summary> /// create a product by All of it's stuff /// </summary> /// <returns></returns> public void CreateProduct(ProductViewModel productViewModel) { var product = productViewModel.ConvertToProductModel(); product.CreationTime = DateTime.Now; _productRepository.Add(product); _productRepository.SaveChanges(); } /// <summary> /// returns product by it's id /// </summary> /// <param name="productId"></param> /// <returns></returns> public ProductViewModel GetProduct(int productId) { var product = _productRepository.FindBy(productId); var productSelected = product.ConvertToProductViewModel(); return productSelected; } /// <summary> /// Update product by Id /// </summary> /// <param name="productViewModel"></param> /// <returns></returns> public void UpdateProduct(ProductViewModel productViewModel) { var product = productViewModel.ConvertToProductModel(); product.CreationTime = DateTime.Now; _productRepository.Update(product); _productRepository.SaveChanges(); } /// <summary> /// remove product service method /// </summary> /// <param name="productId"></param> public void RemoveProduct(int productId) { var product = new Product { Id = productId }; _productRepository.Delete(product); _productRepository.SaveChanges(); } /// <summary>I /// get a product list by category /// </summary> /// <param name="categoryId"></param> /// <returns></returns> public IEnumerable<ProductViewModel> GetAllProductsByCategory(int categoryId) { var product = _productRepository.FindAllProductsByCategory(categoryId); var productView = product.ConvertToProductViewList(); return productView; } /// <summary> /// Get latest 10 product to show in the main page /// </summary> /// <returns></returns> public IEnumerable<ProductViewModel> GetLatestProducts() { var products = _productRepository.FindRecentProducts(3); var productViewMode = products.ConvertToProductViewList(); return productViewMode; } /// <summary> /// returns Products by Brand /// </summary> /// <param name="brandId"></param> /// <returns></returns> public IEnumerable<ProductViewModel> GetAllProductstByBrand(int brandId) { var brands = _productRepository.FindAllProductsByBrand(brandId); var brandsViewModel = brands.ConvertToProductViewList(); return brandsViewModel; } #endregion #region Brand public void CreateBrand(BrandViewModel brandViewModel) { var brands = new List<Brand> { new Brand { Id = brandViewModel.BrandId, Name = brandViewModel.Name } }; var product = new Product(brands); _productRepository.Add(product); _productRepository.SaveChanges(); } /// <summary> /// returns All brand of the website /// </summary> /// <returns></returns> public IEnumerable<BrandViewModel> GetAllBrands() { var brands = _productRepository.FindAllBrands(); var brandViewModel = brands.ConvertToBrandViewModelList(); return brandViewModel; } /// <summary> /// Remove Brand By Identity /// </summary> /// <param name="brandId"></param> public void RemoveBrand(int brandId) { var brand = new Brand { Id = brandId }; if (ModifyIfBrandIsRemovable(brandId)) { // _productRepository.Delete(brand); // _brandRepository.SaveChanges(); } } /// <summary> /// Modifies if brnad is removable or not /// if it belongs to a product or not /// </summary> /// <param name="brandId"></param> /// <returns></returns> public bool ModifyIfBrandIsRemovable(int brandId) { var brand = _productRepository.GetAll(); return brand.All(item => item.BrandId != brandId); } #endregion #region Category /// <summary> /// Create Category Method /// </summary> /// <returns></returns> public void CreateCategory(CategoryViewModel categoryViewModel) { var categories = new List<Category> { new Category { Id = categoryViewModel.CategoryId, Name = categoryViewModel.Name } }; var product = new Product(categories); // product.AddCategory(ca); _productRepository.SaveChanges(); } /// <summary> /// get All Category of the system! /// </summary> /// <returns></returns> public IEnumerable<CategoryViewModel> GetAllCategories() { var category = _productRepository.FindAllCategories(); var categoryViewModel = category.ConvertToCategoryViewModelList(); return categoryViewModel; } /// <summary> /// Remove Category by it's Id /// maybe it never will be used! /// </summary> /// <param name="categoryId"></param> public void RemoveCategory(int categoryId) { var category = new Category { Id = categoryId }; if (ModifyIfCategoryIsRemovable(categoryId)) { //_categoryRepository.Delete(category); //_categoryRepository.SaveChanges(); } } /// <summary> /// Modify if category is removabl /// </summary> /// <param name="categoryId"></param> /// <returns></returns> private bool ModifyIfCategoryIsRemovable(int categoryId) { var brand = _productRepository.GetAll(); return brand.All(item => item.Categories.All(c => c.Id != categoryId)); } #endregion }
To be frank I just saw this implementation after years! And there are some mistakes and uncompleted code, just remember that we should not have repository and service for non-aggregates. In the above sample, category and brand should be deleted via Product repository and the methods of deleting should be in Model just like adding. There is mapper in the service implementation and I've used AutoMapper:
public static class ProductMapper { #region Products /// <summary> /// All product list /// </summary> /// <param name="product"></param> /// <returns></returns> public static IEnumerable<ProductViewModel> ConvertToProductViewList(this IEnumerable<Product> product) { Mapper.CreateMap<Product, ProductViewModel>() .ForMember(pro => pro.ProductId, src => src.MapFrom(p => p.Id)); return Mapper.Map<IEnumerable<Product>, IEnumerable<ProductViewModel>>(product); } /// <summary> /// /// </summary> /// <param name="product"></param> /// <returns></returns> public static ProductViewModel ConvertToProductViewModel(this Product product) { Mapper.CreateMap<Product, ProductViewModel>() .ForMember(pro => pro.ProductId, src => src.MapFrom(p => p.Id)); return Mapper.Map<Product, ProductViewModel>(product); } /// <summary> /// /// </summary> /// <param name="productViewModel"></param> /// <returns></returns> public static Product ConvertToProductModel(this ProductViewModel productViewModel) { Mapper.CreateMap<ProductViewModel, Product>() .ForMember(pro => pro.Id, src => src.MapFrom(p => p.ProductId)); return Mapper.Map<ProductViewModel, Product>(productViewModel); } #endregion #region Brand /// <summary> /// Convert to Brand View Model /// </summary> /// <param name="brand"></param> /// <returns></returns> public static BrandViewModel ConvertToBrandViewModel(this Brand brand) { Mapper.CreateMap<Brand, BrandViewModel>() .ForMember(bra => bra.BrandId, br => br.MapFrom(b => b.Id)); return Mapper.Map<Brand, BrandViewModel>(brand); } /// <summary> /// Convert to Brand View Model List /// </summary> /// <param name="brand"></param> /// <returns></returns> public static IEnumerable<BrandViewModel> ConvertToBrandViewModelList(this IEnumerable<Brand> brand) { Mapper.CreateMap<Brand, BrandViewModel>() .ForMember(bra => bra.BrandId, br => br.MapFrom(b => b.Id)); return Mapper.Map<IEnumerable<Brand>, IEnumerable<BrandViewModel>>(brand); } #endregion #region Category /// <summary> /// Convert To Category ViewModel /// </summary> /// <param name="category"></param> /// <returns></returns> public static CategoryViewModel ConvertToCategoryViewModel(this Category category) { Mapper.CreateMap<Category, CategoryViewModel>() .ForMember(cat => cat.CategoryId, opt => opt.MapFrom(src => src.Id)); return Mapper.Map<Category, CategoryViewModel>(category); } /// <summary> /// Convert To Category ViewModel List /// </summary> /// <param name="category"></param> /// <returns></returns> public static IEnumerable<CategoryViewModel> ConvertToCategoryViewModelList(this IEnumerable<Category> category) { Mapper.CreateMap<Category, CategoryViewModel>() .ForMember(cat => cat.CategoryId, opt => opt.MapFrom(src => src.Id)); return Mapper.Map<IEnumerable<Category>, IEnumerable<CategoryViewModel>>(category); } #endregion }
And finally, there is an MVC application to use the codes we have written! In this project of the solution, there is an area for website administrator for Managing the system. The ProductController implementation in the website (not in management area):
public class ProductController : Controller { private readonly IProductService _productService; public ProductController(IProductService productService) { _productService = productService; } /// <summary> /// Returns List of the Products /// </summary> /// <returns></returns> public ActionResult List(int? page) { var products = _productService.GetAllProducts(); var pageNumber = page ?? 1; var onePageOfProducts = products.ToPagedList(pageNumber, 20); ViewBag.OnePageOfProducts = onePageOfProducts; return View(products); } /// <summary> /// Returns the all Details of the Product /// </summary> /// <returns></returns> public ActionResult Detail(int productId) { var product = _productService.GetProduct(productId); return View("Detail", product); } /// <summary> /// Product Side bar that will show in the main page /// it Returns latest products of the system /// </summary> /// <returns></returns> [ChildActionOnly] public ActionResult ProductSideBar() { var product = _productService.GetLatestProducts(); return PartialView("ProductSideBar", product); } }
And for other parts of the project, you can get the source as I mentioned in the first part of the post. It's worth to say that I just wanted to show you the structure and I didn't upload you the final and production code. Thanks!
Category: Software