diff --git a/.gitignore b/.gitignore index 454729b..6950d24 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ obj bin appsettings.json -*.db \ No newline at end of file +*.db* \ No newline at end of file diff --git a/API/API.csproj b/API/API.csproj index 5bc8cfe..dda2eff 100644 --- a/API/API.csproj +++ b/API/API.csproj @@ -6,6 +6,7 @@ + runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/API/Controllers/ProductsController.cs b/API/Controllers/ProductsController.cs index 735d3e8..fcd46f9 100644 --- a/API/Controllers/ProductsController.cs +++ b/API/Controllers/ProductsController.cs @@ -1,6 +1,9 @@ using Core.Entities; using Microsoft.AspNetCore.Mvc; using Core.Interfaces; +using Core.Specifications; +using API.Dtos; +using AutoMapper; namespace API.Controllers { @@ -8,24 +11,46 @@ namespace API.Controllers [Route("api/[controller]")] public class ProductsController : ControllerBase { - private readonly iProductRepository _repo; - public ProductsController(iProductRepository repo) + private readonly IGenericRepository _productsRepo; + private readonly IGenericRepository _productBrandRepo; + private readonly IGenericRepository _productTypeRepo; + private readonly IMapper _mapper; + public ProductsController(IGenericRepository productsRepo, IGenericRepository productBrandRepo, IGenericRepository productTypeRepo, IMapper mapper) { - _repo = repo; + _mapper = mapper; + _productTypeRepo = productTypeRepo; + _productBrandRepo = productBrandRepo; + _productsRepo = productsRepo; + } [HttpGet] - public async Task>> GetProducts() + public async Task>> GetProducts() { - var products = await _repo.GetProductsAync(); - - return Ok(products); + var spec = new ProductsWithTypesAndBrandsSpecification(); + var products = await _productsRepo.ListAsync(spec); + return Ok(_mapper.Map, IReadOnlyList>(products)); } [HttpGet("{id}")] - public async Task> GetProduct(int id) + public async Task> GetProduct(int id) { - return await _repo.GetProductByIdAsync(id); + var spec = new ProductsWithTypesAndBrandsSpecification(id); + var product = await _productsRepo.GetEntityWithSpec(spec); + return _mapper.Map(product); + } + + [HttpGet("brands")] + public async Task>> GetProductBrands() + { + return Ok(await _productBrandRepo.ListAllAsync()); + } + + [HttpGet("types")] + public async Task>> GetProductTypes() + { + return Ok(await _productTypeRepo.ListAllAsync()); + } } } \ No newline at end of file diff --git a/API/Dtos/ProductToReturnDto.cs b/API/Dtos/ProductToReturnDto.cs new file mode 100644 index 0000000..7a204d8 --- /dev/null +++ b/API/Dtos/ProductToReturnDto.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace API.Dtos +{ + public class ProductToReturnDto + { + public int Id { get; set; } + public string Name { get; set; } + public string Description { get; set; } + public decimal Price { get; set; } + public string PictureUrl { get; set; } + public string ProductType { get; set; } + public string ProductBrand { get; set; } + } +} \ No newline at end of file diff --git a/API/Helpers/MappingProfiles.cs b/API/Helpers/MappingProfiles.cs new file mode 100644 index 0000000..1edaa9d --- /dev/null +++ b/API/Helpers/MappingProfiles.cs @@ -0,0 +1,16 @@ +using API.Dtos; +using AutoMapper; +using Core.Entities; + +namespace API.Helpers +{ + public class MappingProfiles : Profile + { + public MappingProfiles() + { + CreateMap() + .ForMember(d => d.ProductBrand, o => o.MapFrom(s => s.ProductBrand.Name)) + .ForMember(d => d.ProductType, o => o.MapFrom(s => s.ProductType.Name)); + } + } +} \ No newline at end of file diff --git a/API/Program.cs b/API/Program.cs index 2719f5e..11a5d97 100644 --- a/API/Program.cs +++ b/API/Program.cs @@ -1,3 +1,4 @@ +using Infrastructure; using Infrastructure.Data; using Microsoft.EntityFrameworkCore; @@ -16,6 +17,7 @@ namespace API { var context = services.GetRequiredService(); await context.Database.MigrateAsync(); + await StoreContextSeed.SeedAsync(context, loggerFactory); } catch (Exception ex) { diff --git a/API/Startup.cs b/API/Startup.cs index 742c42f..8d9d6fc 100644 --- a/API/Startup.cs +++ b/API/Startup.cs @@ -1,3 +1,4 @@ +using API.Helpers; using Core.Interfaces; using Infrastructure.Data; using Microsoft.EntityFrameworkCore; @@ -21,6 +22,8 @@ namespace API services.AddControllers(); services.AddDbContext(x => x.UseSqlite(_config.GetConnectionString("DefaultConnection"))); services.AddScoped(); + services.AddScoped(typeof(IGenericRepository<>), (typeof(GenericRepository<>))); + services.AddAutoMapper(typeof(MappingProfiles)); services.AddSwaggerGen(c => { c.SwaggerDoc("v1", new OpenApiInfo { Title = "WebAPIv5", Version = "v1" }); diff --git a/API/ecommerce.db-shm b/API/ecommerce.db-shm deleted file mode 100644 index 09e3ea4..0000000 Binary files a/API/ecommerce.db-shm and /dev/null differ diff --git a/Core/Interfaces/IGenericRepository.cs b/Core/Interfaces/IGenericRepository.cs new file mode 100644 index 0000000..10ad70f --- /dev/null +++ b/Core/Interfaces/IGenericRepository.cs @@ -0,0 +1,13 @@ +using Core.Entities; +using Core.Specifications; + +namespace Core.Interfaces +{ + public interface IGenericRepository where T : BaseEntity + { + Task GetByIdAsync(int id); + Task> ListAllAsync(); + Task GetEntityWithSpec(ISpecification spec); + Task> ListAsync(ISpecification spec); + } +} \ No newline at end of file diff --git a/Core/Interfaces/iProductRepository.cs b/Core/Interfaces/iProductRepository.cs index 5e63b17..ce9b413 100644 --- a/Core/Interfaces/iProductRepository.cs +++ b/Core/Interfaces/iProductRepository.cs @@ -6,5 +6,7 @@ namespace Core.Interfaces { Task GetProductByIdAsync(int id); Task> GetProductsAync(); + Task> GetProductBrandsAsync(); + Task> GetProductTypesAsync(); } } \ No newline at end of file diff --git a/Core/Specifications/BaseSpecification.cs b/Core/Specifications/BaseSpecification.cs new file mode 100644 index 0000000..9c241a9 --- /dev/null +++ b/Core/Specifications/BaseSpecification.cs @@ -0,0 +1,23 @@ +using System.Linq.Expressions; + +namespace Core.Specifications +{ + public class BaseSpecification : ISpecification + { + public BaseSpecification() + { + } + + public BaseSpecification(Expression> criteria) + { + Criteria = criteria; + } + public Expression> Criteria { get; } + public List>> Includes { get; } = + new List>>(); + protected void AddInclude(Expression> includeExpression) + { + Includes.Add(includeExpression); + } + } +} \ No newline at end of file diff --git a/Core/Specifications/ISpecification.cs b/Core/Specifications/ISpecification.cs new file mode 100644 index 0000000..b08c329 --- /dev/null +++ b/Core/Specifications/ISpecification.cs @@ -0,0 +1,11 @@ +using System.Linq.Expressions; + +namespace Core.Specifications +{ + public interface ISpecification + { + Expression> Criteria {get; } + List>> Includes {get; } + + } +} \ No newline at end of file diff --git a/Core/Specifications/ProductsWithTypesAndBrandsSpecification.cs b/Core/Specifications/ProductsWithTypesAndBrandsSpecification.cs new file mode 100644 index 0000000..4a6b08a --- /dev/null +++ b/Core/Specifications/ProductsWithTypesAndBrandsSpecification.cs @@ -0,0 +1,20 @@ +using System.Linq.Expressions; +using Core.Entities; + +namespace Core.Specifications +{ + public class ProductsWithTypesAndBrandsSpecification : BaseSpecification + { + public ProductsWithTypesAndBrandsSpecification() + { + AddInclude(x => x.ProductType); + AddInclude(x => x.ProductBrand); + } + + public ProductsWithTypesAndBrandsSpecification(int id) : base(x => x.Id == id) + { + AddInclude(x => x.ProductType); + AddInclude(x => x.ProductBrand); + } + } +} \ No newline at end of file diff --git a/Infrastructure/Data/GenericRepository.cs b/Infrastructure/Data/GenericRepository.cs new file mode 100644 index 0000000..90df1b1 --- /dev/null +++ b/Infrastructure/Data/GenericRepository.cs @@ -0,0 +1,41 @@ +using Core.Entities; +using Core.Interfaces; +using Core.Specifications; +using Microsoft.EntityFrameworkCore; + +namespace Infrastructure.Data +{ + public class GenericRepository : IGenericRepository where T : BaseEntity + { + private readonly StoreContext _context; + public GenericRepository(StoreContext context) + { + _context = context; + } + + public async Task GetByIdAsync(int id) + { + return await _context.Set().FindAsync(id); + } + + + public async Task> ListAllAsync() + { + return await _context.Set().ToListAsync(); + } + public async Task GetEntityWithSpec(ISpecification spec) + { + return await ApplySpecification(spec).FirstOrDefaultAsync(); + } + + public async Task> ListAsync(ISpecification spec) + { + return await ApplySpecification(spec).ToListAsync(); + } + + private IQueryable ApplySpecification(ISpecification spec) + { + return SpecificationEvaluator.GetQuery(_context.Set().AsQueryable(), spec); + } + } +} \ No newline at end of file diff --git a/Infrastructure/Data/ProductRepository.cs b/Infrastructure/Data/ProductRepository.cs index cf7b2f1..88e70e1 100644 --- a/Infrastructure/Data/ProductRepository.cs +++ b/Infrastructure/Data/ProductRepository.cs @@ -12,14 +12,30 @@ namespace Infrastructure.Data _context = context; } + public async Task> GetProductBrandsAsync() + { + return await _context.ProductBrands.ToListAsync(); + } + public async Task GetProductByIdAsync(int id) { - return await _context.Products.FindAsync(id); + return await _context.Products + .Include(p => p.ProductType) + .Include(p => p.ProductBrand) + .FirstOrDefaultAsync(p => p.Id == id); } public async Task> GetProductsAync() { - return await _context.Products.ToListAsync(); + return await _context.Products + .Include(p => p.ProductType) + .Include(p => p.ProductBrand) + .ToListAsync(); + } + + public async Task> GetProductTypesAsync() + { + return await _context.ProductTypes.ToListAsync(); } } } \ No newline at end of file diff --git a/Infrastructure/Data/SeedData/brands.json b/Infrastructure/Data/SeedData/brands.json new file mode 100644 index 0000000..592b241 --- /dev/null +++ b/Infrastructure/Data/SeedData/brands.json @@ -0,0 +1,26 @@ +[ + { + "Id": 1, + "Name": "Angular" + }, + { + "Id": 2, + "Name": "NetCore" + }, + { + "Id": 3, + "Name": "VS Code" + }, + { + "Id": 4, + "Name": "React" + }, + { + "Id": 5, + "Name": "Typescript" + }, + { + "Id": 6, + "Name": "Redis" + } +] \ No newline at end of file diff --git a/Infrastructure/Data/SeedData/products.json b/Infrastructure/Data/SeedData/products.json new file mode 100644 index 0000000..600ecfc --- /dev/null +++ b/Infrastructure/Data/SeedData/products.json @@ -0,0 +1,146 @@ +[ + { + "Name": "Angular Speedster Board 2000", + "Description": "Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas porttitor congue massa. Fusce posuere, magna sed pulvinar ultricies, purus lectus malesuada libero, sit amet commodo magna eros quis urna.", + "Price": 200, + "PictureUrl": "images/products/sb-ang1.png", + "ProductTypeId": 1, + "ProductBrandId": 1 + }, + { + "Name": "Green Angular Board 3000", + "Description": "Nunc viverra imperdiet enim. Fusce est. Vivamus a tellus.", + "Price": 150, + "PictureUrl": "images/products/sb-ang2.png", + "ProductTypeId": 1, + "ProductBrandId": 1 + }, + { + "Name": "Core Board Speed Rush 3", + "Description": "Suspendisse dui purus, scelerisque at, vulputate vitae, pretium mattis, nunc. Mauris eget neque at sem venenatis eleifend. Ut nonummy.", + "Price": 180, + "PictureUrl": "images/products/sb-core1.png", + "ProductTypeId": 1, + "ProductBrandId": 2 + }, + { + "Name": "Net Core Super Board", + "Description": "Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Proin pharetra nonummy pede. Mauris et orci.", + "Price": 300, + "PictureUrl": "images/products/sb-core2.png", + "ProductTypeId": 1, + "ProductBrandId": 2 + }, + { + "Name": "React Board Super Whizzy Fast", + "Description": "Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas porttitor congue massa. Fusce posuere, magna sed pulvinar ultricies, purus lectus malesuada libero, sit amet commodo magna eros quis urna.", + "Price": 250, + "PictureUrl": "images/products/sb-react1.png", + "ProductTypeId": 1, + "ProductBrandId": 4 + }, + { + "Name": "Typescript Entry Board", + "Description": "Aenean nec lorem. In porttitor. Donec laoreet nonummy augue.", + "Price": 120, + "PictureUrl": "images/products/sb-ts1.png", + "ProductTypeId": 1, + "ProductBrandId": 5 + }, + { + "Name": "Core Blue Hat", + "Description": "Fusce posuere, magna sed pulvinar ultricies, purus lectus malesuada libero, sit amet commodo magna eros quis urna.", + "Price": 10, + "PictureUrl": "images/products/hat-core1.png", + "ProductTypeId": 2, + "ProductBrandId": 2 + }, + { + "Name": "Green React Woolen Hat", + "Description": "Suspendisse dui purus, scelerisque at, vulputate vitae, pretium mattis, nunc. Mauris eget neque at sem venenatis eleifend. Ut nonummy.", + "Price": 8, + "PictureUrl": "images/products/hat-react1.png", + "ProductTypeId": 2, + "ProductBrandId": 4 + }, + { + "Name": "Purple React Woolen Hat", + "Description": "Fusce posuere, magna sed pulvinar ultricies, purus lectus malesuada libero, sit amet commodo magna eros quis urna.", + "Price": 15, + "PictureUrl": "images/products/hat-react2.png", + "ProductTypeId": 2, + "ProductBrandId": 4 + }, + { + "Name": "Blue Code Gloves", + "Description": "Nunc viverra imperdiet enim. Fusce est. Vivamus a tellus.", + "Price": 18, + "PictureUrl": "images/products/glove-code1.png", + "ProductTypeId": 4, + "ProductBrandId": 3 + }, + { + "Name": "Green Code Gloves", + "Description": "Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Proin pharetra nonummy pede. Mauris et orci.", + "Price": 15, + "PictureUrl": "images/products/glove-code2.png", + "ProductTypeId": 4, + "ProductBrandId": 3 + }, + { + "Name": "Purple React Gloves", + "Description": "Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas porttitor congue massa.", + "Price": 16, + "PictureUrl": "images/products/glove-react1.png", + "ProductTypeId": 4, + "ProductBrandId": 4 + }, + { + "Name": "Green React Gloves", + "Description": "Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Proin pharetra nonummy pede. Mauris et orci.", + "Price": 14, + "PictureUrl": "images/products/glove-react2.png", + "ProductTypeId": 4, + "ProductBrandId": 4 + }, + { + "Name": "Redis Red Boots", + "Description": "Suspendisse dui purus, scelerisque at, vulputate vitae, pretium mattis, nunc. Mauris eget neque at sem venenatis eleifend. Ut nonummy.", + "Price": 250, + "PictureUrl": "images/products/boot-redis1.png", + "ProductTypeId": 3, + "ProductBrandId": 6 + }, + { + "Name": "Core Red Boots", + "Description": "Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas porttitor congue massa. Fusce posuere, magna sed pulvinar ultricies, purus lectus malesuada libero, sit amet commodo magna eros quis urna.", + "Price": 189.99, + "PictureUrl": "images/products/boot-core2.png", + "ProductTypeId": 3, + "ProductBrandId": 2 + }, + { + "Name": "Core Purple Boots", + "Description": "Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Proin pharetra nonummy pede. Mauris et orci.", + "Price": 199.99, + "PictureUrl": "images/products/boot-core1.png", + "ProductTypeId": 3, + "ProductBrandId": 2 + }, + { + "Name": "Angular Purple Boots", + "Description": "Aenean nec lorem. In porttitor. Donec laoreet nonummy augue.", + "Price": 150, + "PictureUrl": "images/products/boot-ang2.png", + "ProductTypeId": 3, + "ProductBrandId": 1 + }, + { + "Name": "Angular Blue Boots", + "Description": "Suspendisse dui purus, scelerisque at, vulputate vitae, pretium mattis, nunc. Mauris eget neque at sem venenatis eleifend. Ut nonummy.", + "Price": 180, + "PictureUrl": "images/products/boot-ang1.png", + "ProductTypeId": 3, + "ProductBrandId": 1 + } +] \ No newline at end of file diff --git a/Infrastructure/Data/SeedData/types.json b/Infrastructure/Data/SeedData/types.json new file mode 100644 index 0000000..9ae0d91 --- /dev/null +++ b/Infrastructure/Data/SeedData/types.json @@ -0,0 +1,18 @@ +[ + { + "Id": 1, + "Name": "Boards" + }, + { + "Id": 2, + "Name": "Hats" + }, + { + "Id": 3, + "Name": "Boots" + }, + { + "Id": 4, + "Name": "Gloves" + } +] \ No newline at end of file diff --git a/Infrastructure/Data/SpecificationEvaluator.cs b/Infrastructure/Data/SpecificationEvaluator.cs new file mode 100644 index 0000000..a59d0b2 --- /dev/null +++ b/Infrastructure/Data/SpecificationEvaluator.cs @@ -0,0 +1,23 @@ +using Core.Entities; +using Core.Specifications; +using Microsoft.EntityFrameworkCore; + +namespace Infrastructure.Data +{ + public class SpecificationEvaluator where TEntity : BaseEntity + { + public static IQueryable GetQuery(IQueryable inputQuery, ISpecification spec) + { + var query = inputQuery; + + if (spec.Criteria != null) + { + query = query.Where(spec.Criteria); + } + + query = spec.Includes.Aggregate(query, (current, include) => current.Include(include)); + + return query; + } + } +} \ No newline at end of file diff --git a/Infrastructure/Data/StoreContextSeed.cs b/Infrastructure/Data/StoreContextSeed.cs new file mode 100644 index 0000000..de4c058 --- /dev/null +++ b/Infrastructure/Data/StoreContextSeed.cs @@ -0,0 +1,57 @@ +using System.Text.Json; +using Core.Entities; +using Infrastructure.Data; +using Microsoft.Extensions.Logging; + +namespace Infrastructure +{ + public class StoreContextSeed + { + public static async Task SeedAsync(StoreContext context, ILoggerFactory loggerFactory) + { + try + { + if (!context.ProductBrands.Any()) + { + var brandsData = File.ReadAllText("../Infrastructure/Data/SeedData/brands.json"); + var brands = JsonSerializer.Deserialize>(brandsData); + foreach (var item in brands) + { + context.ProductBrands.Add(item); + } + + await context.SaveChangesAsync(); + } + + if (!context.ProductTypes.Any()) + { + var typesData = File.ReadAllText("../Infrastructure/Data/SeedData/types.json"); + var types = JsonSerializer.Deserialize>(typesData); + foreach (var item in types) + { + context.ProductTypes.Add(item); + } + + await context.SaveChangesAsync(); + } + + if (!context.Products.Any()) + { + var productsData = File.ReadAllText("../Infrastructure/Data/SeedData/products.json"); + var products = JsonSerializer.Deserialize>(productsData); + foreach (var item in products) + { + context.Products.Add(item); + } + + await context.SaveChangesAsync(); + } + } + catch (Exception ex) + { + var logger = loggerFactory.CreateLogger(); + logger.LogError(ex.Message); + } + } + } +} \ No newline at end of file