From 5f9330f20b100290bee986c77aee3b0828bd175c Mon Sep 17 00:00:00 2001 From: Charles Showalter Date: Wed, 11 May 2022 16:24:26 -0700 Subject: [PATCH] Added Sorting, Filtering, Pagination and Cors --- API/Controllers/ProductsController.cs | 10 +++++-- .../ApplicationServicesExtensions.cs | 2 +- API/Helpers/Pagination.cs | 23 +++++++++++++++ API/Startup.cs | 15 ++++++---- Core/Interfaces/IGenericRepository.cs | 1 + Core/Specifications/BaseSpecification.cs | 28 +++++++++++++++++++ Core/Specifications/ISpecification.cs | 11 ++++++-- Core/Specifications/ProductSpecParams.cs | 25 +++++++++++++++++ ...roductWithFiltersForCountSpecifications.cs | 16 +++++++++++ ...ProductsWithTypesAndBrandsSpecification.cs | 24 +++++++++++++++- Infrastructure/Data/GenericRepository.cs | 4 +++ Infrastructure/Data/SpecificationEvaluator.cs | 15 ++++++++++ Infrastructure/Data/StoreContext.cs | 12 ++++++++ 13 files changed, 172 insertions(+), 14 deletions(-) create mode 100644 API/Helpers/Pagination.cs create mode 100644 Core/Specifications/ProductSpecParams.cs create mode 100644 Core/Specifications/ProductWithFiltersForCountSpecifications.cs diff --git a/API/Controllers/ProductsController.cs b/API/Controllers/ProductsController.cs index 86a09d5..19a6d50 100644 --- a/API/Controllers/ProductsController.cs +++ b/API/Controllers/ProductsController.cs @@ -5,6 +5,7 @@ using Core.Specifications; using API.Dtos; using AutoMapper; using API.Errors; +using API.Helpers; namespace API.Controllers { @@ -24,11 +25,14 @@ namespace API.Controllers } [HttpGet] - public async Task>> GetProducts() + public async Task>>> GetProducts([FromQuery]ProductSpecParams productParams) { - var spec = new ProductsWithTypesAndBrandsSpecification(); + var spec = new ProductsWithTypesAndBrandsSpecification(productParams); + var countSpec = new ProductWithFiltersForCountSpecifications(productParams); + var totalItems = await _productsRepo.CountAsync(countSpec); var products = await _productsRepo.ListAsync(spec); - return Ok(_mapper.Map, IReadOnlyList>(products)); + var data = _mapper.Map, IReadOnlyList>(products); + return Ok(new Pagination(productParams.PageIndex, productParams.PageSize, totalItems, data)); } [HttpGet("{id}")] diff --git a/API/Extensions/ApplicationServicesExtensions.cs b/API/Extensions/ApplicationServicesExtensions.cs index 532ef9f..a870430 100644 --- a/API/Extensions/ApplicationServicesExtensions.cs +++ b/API/Extensions/ApplicationServicesExtensions.cs @@ -5,7 +5,7 @@ using Microsoft.AspNetCore.Mvc; namespace API.Extensions { - public static class ApplicationServicesExtensions + public static class ApplicationServicesExtensions { public static IServiceCollection AddApplicationServices(this IServiceCollection services) { diff --git a/API/Helpers/Pagination.cs b/API/Helpers/Pagination.cs new file mode 100644 index 0000000..31c9394 --- /dev/null +++ b/API/Helpers/Pagination.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace API.Helpers +{ + public class Pagination where T : class + { + public Pagination(int pageIndex, int pageSize, int count, IReadOnlyList data) + { + PageIndex = pageIndex; + PageSize = pageSize; + Count = count; + Data = data; + } + + public int PageIndex { get; set; } + public int PageSize { get; set; } + public int Count { get; set; } + public IReadOnlyList Data {get; set;} + } +} \ No newline at end of file diff --git a/API/Startup.cs b/API/Startup.cs index 6fe1c3a..b8d3d3a 100644 --- a/API/Startup.cs +++ b/API/Startup.cs @@ -24,24 +24,27 @@ namespace API services.AddSwaggerDocumentation(); services.AddDbContext(x => x.UseSqlite(_config.GetConnectionString("DefaultConnection"))); services.AddAutoMapper(typeof(MappingProfiles)); - + services.AddCors(opt => + { + opt.AddPolicy("CorsPolicy", policy => + { + policy.AllowAnyHeader().AllowAnyMethod().WithOrigins("https://localhost:4200"); + }); + }); + } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { app.UseMiddleware(); - app.UseStatusCodePagesWithReExecute("/errors/{0}"); - app.UseHttpsRedirection(); - app.UseRouting(); app.UseStaticFiles(); - + app.UseCors("CorsPolicy"); app.UseAuthorization(); app.UseSwaggerDocumentation(); - app.UseEndpoints(endpoints => { endpoints.MapControllers(); diff --git a/Core/Interfaces/IGenericRepository.cs b/Core/Interfaces/IGenericRepository.cs index 10ad70f..e750a6d 100644 --- a/Core/Interfaces/IGenericRepository.cs +++ b/Core/Interfaces/IGenericRepository.cs @@ -9,5 +9,6 @@ namespace Core.Interfaces Task> ListAllAsync(); Task GetEntityWithSpec(ISpecification spec); Task> ListAsync(ISpecification spec); + Task CountAsync(ISpecification spec); } } \ No newline at end of file diff --git a/Core/Specifications/BaseSpecification.cs b/Core/Specifications/BaseSpecification.cs index 9c241a9..ddbf509 100644 --- a/Core/Specifications/BaseSpecification.cs +++ b/Core/Specifications/BaseSpecification.cs @@ -15,9 +15,37 @@ namespace Core.Specifications public Expression> Criteria { get; } public List>> Includes { get; } = new List>>(); + + public Expression> OrderBy {get; private set;} + + public Expression> OrderByDecending {get; private set;} + + public int Take {get; private set;} + + public int Skip {get; private set;} + + public bool IsPagingEnabled {get; private set;} + protected void AddInclude(Expression> includeExpression) { Includes.Add(includeExpression); } + + protected void AddOrderBy(Expression> orderByExpression) + { + OrderBy = orderByExpression; + } + + protected void AddOrdeByDescending(Expression> orderByDescExpression) + { + OrderByDecending = orderByDescExpression; + } + + protected void ApplyPaging(int skip, int take) + { + Skip = skip; + Take = take; + IsPagingEnabled = true; + } } } \ No newline at end of file diff --git a/Core/Specifications/ISpecification.cs b/Core/Specifications/ISpecification.cs index b08c329..980d39d 100644 --- a/Core/Specifications/ISpecification.cs +++ b/Core/Specifications/ISpecification.cs @@ -4,8 +4,13 @@ namespace Core.Specifications { public interface ISpecification { - Expression> Criteria {get; } - List>> Includes {get; } - + Expression> Criteria { get; } + List>> Includes { get; } + Expression> OrderBy { get; } + Expression> OrderByDecending { get; } + + int Take {get; } + int Skip {get; } + bool IsPagingEnabled {get; } } } \ No newline at end of file diff --git a/Core/Specifications/ProductSpecParams.cs b/Core/Specifications/ProductSpecParams.cs new file mode 100644 index 0000000..9a45f4b --- /dev/null +++ b/Core/Specifications/ProductSpecParams.cs @@ -0,0 +1,25 @@ +namespace Core.Specifications +{ + public class ProductSpecParams + { + private const int MaxPageSize = 50; + public int PageIndex {get; set;} =1; + + private int _pageSize = 6; + public int PageSize + { + get => _pageSize; + set => _pageSize = (value > MaxPageSize) ? MaxPageSize : value; + } + public int? BrandId { get; set; } + public int? TypeId { get; set; } + public string Sort { get; set; } + + private string _search; + public string Search + { + get => _search; + set => _search = value.ToLower(); + } + } +} \ No newline at end of file diff --git a/Core/Specifications/ProductWithFiltersForCountSpecifications.cs b/Core/Specifications/ProductWithFiltersForCountSpecifications.cs new file mode 100644 index 0000000..1617cbe --- /dev/null +++ b/Core/Specifications/ProductWithFiltersForCountSpecifications.cs @@ -0,0 +1,16 @@ +using Core.Entities; + +namespace Core.Specifications +{ + public class ProductWithFiltersForCountSpecifications : BaseSpecification + { + public ProductWithFiltersForCountSpecifications(ProductSpecParams productParams) + : base(x => + (string.IsNullOrEmpty(productParams.Search) || x.Name.ToLower().Contains(productParams.Search)) && + (!productParams.BrandId.HasValue || x.ProductBrandId == productParams.BrandId) && + (!productParams.TypeId.HasValue || x.ProductTypeId == productParams.TypeId) + ) + { + } + } +} \ No newline at end of file diff --git a/Core/Specifications/ProductsWithTypesAndBrandsSpecification.cs b/Core/Specifications/ProductsWithTypesAndBrandsSpecification.cs index 4a6b08a..8d11b39 100644 --- a/Core/Specifications/ProductsWithTypesAndBrandsSpecification.cs +++ b/Core/Specifications/ProductsWithTypesAndBrandsSpecification.cs @@ -5,10 +5,32 @@ namespace Core.Specifications { public class ProductsWithTypesAndBrandsSpecification : BaseSpecification { - public ProductsWithTypesAndBrandsSpecification() + public ProductsWithTypesAndBrandsSpecification(ProductSpecParams productParams) + : base(x => + (string.IsNullOrEmpty(productParams.Search) || x.Name.ToLower().Contains(productParams.Search)) && + (!productParams.BrandId.HasValue || x.ProductBrandId == productParams.BrandId) && + (!productParams.TypeId.HasValue || x.ProductTypeId == productParams.TypeId) + ) { AddInclude(x => x.ProductType); AddInclude(x => x.ProductBrand); + ApplyPaging(productParams.PageSize * (productParams.PageIndex -1), productParams.PageSize); + + if (!string.IsNullOrEmpty(productParams.Sort)) + { + switch (productParams.Sort) + { + case "priceAsc": + AddOrderBy(p => p.Price); + break; + case "priceDesc": + AddOrdeByDescending(p => p.Price); + break; + default: + AddOrderBy(n => n.Name); + break; + } + } } public ProductsWithTypesAndBrandsSpecification(int id) : base(x => x.Id == id) diff --git a/Infrastructure/Data/GenericRepository.cs b/Infrastructure/Data/GenericRepository.cs index 90df1b1..eb8b643 100644 --- a/Infrastructure/Data/GenericRepository.cs +++ b/Infrastructure/Data/GenericRepository.cs @@ -32,6 +32,10 @@ namespace Infrastructure.Data { return await ApplySpecification(spec).ToListAsync(); } + public async Task CountAsync(ISpecification spec) + { + return await ApplySpecification(spec).CountAsync(); + } private IQueryable ApplySpecification(ISpecification spec) { diff --git a/Infrastructure/Data/SpecificationEvaluator.cs b/Infrastructure/Data/SpecificationEvaluator.cs index a59d0b2..5543495 100644 --- a/Infrastructure/Data/SpecificationEvaluator.cs +++ b/Infrastructure/Data/SpecificationEvaluator.cs @@ -15,6 +15,21 @@ namespace Infrastructure.Data query = query.Where(spec.Criteria); } + if (spec.OrderBy != null) + { + query = query.OrderBy(spec.OrderBy); + } + + if (spec.OrderByDecending != null) + { + query = query.OrderByDescending(spec.OrderByDecending); + } + + if (spec.IsPagingEnabled) + { + query = query.Skip(spec.Skip).Take(spec.Take); + } + query = spec.Includes.Aggregate(query, (current, include) => current.Include(include)); return query; diff --git a/Infrastructure/Data/StoreContext.cs b/Infrastructure/Data/StoreContext.cs index 097fbd8..539dca4 100644 --- a/Infrastructure/Data/StoreContext.cs +++ b/Infrastructure/Data/StoreContext.cs @@ -16,6 +16,18 @@ namespace Infrastructure.Data { base.OnModelCreating(modelBuilder); modelBuilder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly()); + + if(Database.ProviderName == "Microsoft.EntityFrameworkCore.Sqlite") + { + foreach (var entityType in modelBuilder.Model.GetEntityTypes()) + { + var properties = entityType.ClrType.GetProperties().Where(p => p.PropertyType == typeof(decimal)); + foreach (var property in properties) + { + modelBuilder.Entity(entityType.Name).Property(property.Name).HasConversion(); + } + } + } } } } \ No newline at end of file