diff --git a/Server/Configurations/MapperInitializer.cs b/Server/Configurations/MapperInitializer.cs index 84b7433..a0a9e8d 100644 --- a/Server/Configurations/MapperInitializer.cs +++ b/Server/Configurations/MapperInitializer.cs @@ -1,3 +1,4 @@ +using System.Dynamic; using AutoMapper; using Server.Models; using SharedModels.DataTransferObjects; @@ -12,9 +13,12 @@ public class MapperInitializer : Profile CreateMap().ReverseMap(); CreateMap().ReverseMap(); CreateMap().ReverseMap(); + CreateMap().ReverseMap(); CreateMap().ReverseMap(); CreateMap().ReverseMap(); + CreateMap().ReverseMap(); + CreateMap().ReverseMap(); CreateMap().ReverseMap(); CreateMap().ReverseMap(); diff --git a/Server/Controllers/StateManagementController.cs b/Server/Controllers/StateManagementController.cs new file mode 100644 index 0000000..29cab71 --- /dev/null +++ b/Server/Controllers/StateManagementController.cs @@ -0,0 +1,102 @@ +using Microsoft.AspNetCore.Mvc; +using Newtonsoft.Json; +using Server.Services; +using SharedModels.DataTransferObjects; +using SharedModels.QueryStringParameters; + +namespace Server.Controllers; + +[Route("api/states")] +[ApiController] +public class StateManagementController : ControllerBase +{ + private readonly IStateManagementService _stateManagementService; + + public StateManagementController(IStateManagementService stateManagementService) + { + _stateManagementService = stateManagementService; + } + + [HttpPost] + public async Task AddState(CreateStateDto state) + { + var result = await _stateManagementService.AddState(state); + + if (!result.isSucceed) + { + return BadRequest(result.message); + } + + return CreatedAtAction(nameof(GetState), new {id = result.state.Id}, result.state); + } + + [HttpGet] + public async Task GetStates([FromQuery] StateParameters parameters) + { + var result = await _stateManagementService.GetStates(parameters); + + if (!result.isSucceed) + { + return BadRequest(result.message); + } + + Response.Headers.Add("X-Pagination", JsonConvert.SerializeObject(result.pagingMetadata)); + + return Ok(result.states); + } + + [HttpGet("{id}")] + public async Task GetState(int id, [FromQuery] string? fields) + { + if (!await _stateManagementService.IsStateExists(id)) + { + return NotFound(); + } + + var result = await _stateManagementService.GetState(id, fields); + + if (!result.isSucceed) + { + return BadRequest(result.message); + } + + return Ok(result.state); + } + + [HttpPut("{id}")] + public async Task UpdateRoute(int id, UpdateStateDto state) + { + if (id != state.Id) + { + return BadRequest(); + } + + var result = await _stateManagementService.UpdateState(state); + + if (!result.isSucceed) + { + return BadRequest(result.message); + } + + return Ok(result.state); + } + + [HttpDelete("{id}")] + public async Task DeleteRoute(int id) + { + if (!await _stateManagementService.IsStateExists(id)) + { + return NotFound(); + } + + var result = await _stateManagementService.DeleteState(id); + + if (!result.isSucceed) + { + return BadRequest(result.message); + } + + return NoContent(); + } +} + diff --git a/Server/Program.cs b/Server/Program.cs index 844aef8..9bfa1e9 100644 --- a/Server/Program.cs +++ b/Server/Program.cs @@ -19,7 +19,7 @@ var builder = WebApplication.CreateBuilder(args); builder.Services.AddControllers().AddNewtonsoftJson(options => { options.SerializerSettings.Formatting = Formatting.Indented; options.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver(); - + options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Error; }); // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle @@ -87,12 +87,15 @@ builder.Services.AddAuthorization(); builder.Services.AddAutoMapper(typeof(MapperInitializer)); builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped, SortHelper>(); +builder.Services.AddScoped, SortHelper>(); builder.Services.AddScoped, DataShaper>(); +builder.Services.AddScoped, DataShaper>(); // Adding DB Context with PostgreSQL var connectionString = builder.Configuration.GetConnectionString("DefaultConnection"); diff --git a/Server/Services/CountryManagementService.cs b/Server/Services/CountryManagementService.cs index ed4f3a0..58b1bdb 100644 --- a/Server/Services/CountryManagementService.cs +++ b/Server/Services/CountryManagementService.cs @@ -1,7 +1,5 @@ using System.Dynamic; -using System.Linq.Dynamic.Core; using AutoMapper; -using AutoMapper.QueryableExtensions; using Microsoft.EntityFrameworkCore; using Server.Data; using Server.Helpers; @@ -38,10 +36,12 @@ public class CountryManagementService : ICountryManagementService return (true, String.Empty, _mapper.Map(country)); } - public async Task<(bool isSucceed, string message, IEnumerable countries, + public async Task<(bool isSucceed, string message, IEnumerable countries, PagingMetadata pagingMetadata)> GetCountries(CountryParameters parameters) { - var dbCountries = _dbContext.Countries.AsQueryable(); + var dbCountries = _dbContext.Countries.Include(c => c.States) + .ThenInclude(s => s.Cities).ThenInclude(c => c.Addresses) + .AsQueryable(); SearchByAllCountryFields(ref dbCountries, parameters.Search); SearchByCountryCode(ref dbCountries, parameters.Code); @@ -64,8 +64,9 @@ public class CountryManagementService : ICountryManagementService parameters.PageSize); var shapedCountiesData = _countryDataShaper.ShapeData(dbCountries, parameters.Fields); + var countryDtos = shapedCountiesData.ToList().ConvertAll(d => _mapper.Map(d)); - return (true, "", shapedCountiesData, pagingMetadata); + return (true, "", countryDtos, pagingMetadata); void SearchByAllCountryFields(ref IQueryable countries, string? search) @@ -74,12 +75,10 @@ public class CountryManagementService : ICountryManagementService { return; } - - var s = search.Trim().ToLower(); countries = countries.Where(c => - c.Code.ToLower().Contains(search) || - c.Name.ToLower().Contains(search)); + c.Code.ToLower().Contains(search.ToLower()) || + c.Name.ToLower().Contains(search.ToLower())); } void SearchByCountryCode(ref IQueryable countries, @@ -110,29 +109,37 @@ public class CountryManagementService : ICountryManagementService int pageNumber, int pageSize) { var metadata = new PagingMetadata(countries, - parameters.PageNumber, parameters.PageSize); + pageNumber, pageSize); countries = countries - .Skip((parameters.PageNumber - 1) * parameters.PageSize) - .Take(parameters.PageSize); + .Skip((pageNumber - 1) * pageSize) + .Take(pageSize); return metadata; } } - public async Task<(bool isSucceed, string message, ExpandoObject country)> GetCountry(int id, string? fields) + public async Task<(bool isSucceed, string message, CountryDto country)> GetCountry(int id, string? fields) { var dbCountry = await _dbContext.Countries.Where(c => c.Id == id) + .Include(c => c.States).ThenInclude(s => s.Cities) + .ThenInclude(c => c.Addresses) .FirstOrDefaultAsync(); if (dbCountry == null) { return (false, $"Country doesn't exist", null)!; } + + if (String.IsNullOrWhiteSpace(fields)) + { + fields = CountryParameters.DefaultFields; + } var shapedCountryData = _countryDataShaper.ShapeData(dbCountry, fields); + var countryDto = _mapper.Map(shapedCountryData); - return (true, "", shapedCountryData); + return (true, "", countryDto); } public async Task<(bool isSucceed, string message, UpdateCountryDto country)> UpdateCountry(UpdateCountryDto updateCountryDto) diff --git a/Server/Services/ICountryManagementService.cs b/Server/Services/ICountryManagementService.cs index a75871c..f320171 100644 --- a/Server/Services/ICountryManagementService.cs +++ b/Server/Services/ICountryManagementService.cs @@ -1,4 +1,3 @@ -using System.Dynamic; using Server.Models; using SharedModels.DataTransferObjects; using SharedModels.QueryStringParameters; @@ -9,9 +8,9 @@ public interface ICountryManagementService { Task<(bool isSucceed, string message, CountryDto country)> AddCountry(CreateCountryDto createCountryDto); - Task<(bool isSucceed, string message, IEnumerable countries, + Task<(bool isSucceed, string message, IEnumerable countries, PagingMetadata pagingMetadata)> GetCountries(CountryParameters parameters); - Task<(bool isSucceed, string message, ExpandoObject country)> GetCountry(int id, string? fields); + Task<(bool isSucceed, string message, CountryDto country)> GetCountry(int id, string? fields); Task<(bool isSucceed, string message, UpdateCountryDto country)> UpdateCountry(UpdateCountryDto updateCountryDto); Task<(bool isSucceed, string message)> DeleteCountry(int id); Task IsCountryExists(int id); diff --git a/Server/Services/IStateManagementService.cs b/Server/Services/IStateManagementService.cs new file mode 100644 index 0000000..5d17b60 --- /dev/null +++ b/Server/Services/IStateManagementService.cs @@ -0,0 +1,17 @@ +using Server.Models; +using SharedModels.DataTransferObjects; +using SharedModels.QueryStringParameters; + +namespace Server.Services; + +public interface IStateManagementService +{ + Task<(bool isSucceed, string message, StateDto state)> AddState(CreateStateDto createStateDto); + + Task<(bool isSucceed, string message, IEnumerable states, + PagingMetadata pagingMetadata)> GetStates(StateParameters parameters); + Task<(bool isSucceed, string message, StateDto state)> GetState(int id, string? fields); + Task<(bool isSucceed, string message, UpdateStateDto state)> UpdateState(UpdateStateDto updateStateDto); + Task<(bool isSucceed, string message)> DeleteState(int id); + Task IsStateExists(int id); +} \ No newline at end of file diff --git a/Server/Services/StateManagementService.cs b/Server/Services/StateManagementService.cs new file mode 100644 index 0000000..243466c --- /dev/null +++ b/Server/Services/StateManagementService.cs @@ -0,0 +1,188 @@ +using System.Dynamic; +using AutoMapper; +using Microsoft.EntityFrameworkCore; +using Server.Data; +using Server.Helpers; +using Server.Models; +using SharedModels.DataTransferObjects; +using SharedModels.QueryStringParameters; + +namespace Server.Services; + +public class StateManagementService : IStateManagementService +{ + private readonly ApplicationDbContext _dbContext; + private readonly IMapper _mapper; + private readonly ISortHelper _stateSortHelper; + private readonly IDataShaper _stateDataShaper; + + public StateManagementService(ApplicationDbContext dbContext, + IMapper mapper, ISortHelper stateSortHelper, + IDataShaper stateDataShaper) + { + _dbContext = dbContext; + _mapper = mapper; + _stateSortHelper = stateSortHelper; + _stateDataShaper = stateDataShaper; + } + + public async Task<(bool isSucceed, string message, StateDto state)> AddState(CreateStateDto createStateDto) + { + var state = _mapper.Map(createStateDto); + + await _dbContext.States.AddAsync(state); + await _dbContext.SaveChangesAsync(); + + return (true, String.Empty, _mapper.Map(state)); + } + + public async Task<(bool isSucceed, string message, IEnumerable states, + PagingMetadata pagingMetadata)> GetStates(StateParameters parameters) + { + var dbStates = _dbContext.States.Include(s => s.Country) + .Include(s => s.Cities) + .ThenInclude(c => c.Addresses).AsQueryable(); + + SearchByAllStateFields(ref dbStates, parameters.Search); + SearchByStateName(ref dbStates, parameters.Name); + SearchByStateCountryId(ref dbStates, parameters.CountryId); + + try + { + dbStates = _stateSortHelper.ApplySort(dbStates, parameters.Sort); + + // By calling Any() we will check if LINQ to Entities Query will be + // executed. If not it will throw an InvalidOperationException exception + var isExecuted = dbStates.Any(); + } + catch (Exception e) + { + return (false, "Invalid sorting string", null, null)!; + } + + var pagingMetadata = ApplyPaging(ref dbStates, parameters.PageNumber, + parameters.PageSize); + + var shapedStatesData = _stateDataShaper.ShapeData(dbStates, parameters.Fields); + var stateDtos = shapedStatesData.ToList().ConvertAll(s => _mapper.Map(s)); + + return (true, "", stateDtos, pagingMetadata); + + void SearchByAllStateFields(ref IQueryable states, + string? search) + { + if (!states.Any() || String.IsNullOrWhiteSpace(search)) + { + return; + } + + states = states.Where(s => + s.Name.ToLower().Contains(search.ToLower())); + } + + void SearchByStateCountryId(ref IQueryable states, + int? countryId) + { + if (!states.Any() || countryId == null) + { + return; + } + + states = states.Where(s => + s.CountryId.ToString().ToLower() + .Contains(countryId.ToString()!.Trim().ToLower())); + } + + void SearchByStateName(ref IQueryable states, + string? stateName) + { + if (!states.Any() || String.IsNullOrWhiteSpace(stateName)) + { + return; + } + + states = states.Where(s => + s.Name.ToLower().Contains(stateName.Trim().ToLower())); + } + + PagingMetadata ApplyPaging(ref IQueryable states, + int pageNumber, int pageSize) + { + var metadata = new PagingMetadata(states, + pageNumber, pageSize); + + states = states + .Skip((pageNumber - 1) * pageSize) + .Take(pageSize); + + return metadata; + } + } + + public async Task<(bool isSucceed, string message, StateDto state)> GetState(int id, string? fields) + { + var dbState = await _dbContext.States.Where(s => s.Id == id) + .Include(s => s.Country).Include(s => s.Cities) + .ThenInclude(c => c.Addresses) + .FirstOrDefaultAsync(); + + if (dbState == null) + { + return (false, $"State doesn't exist", null)!; + } + + if (String.IsNullOrWhiteSpace(fields)) + { + fields = StateParameters.DefaultFields; + } + + var shapedStateData = _stateDataShaper.ShapeData(dbState, fields); + var stateDto = _mapper.Map(shapedStateData); + + return (true, "", stateDto); + } + + public async Task<(bool isSucceed, string message, UpdateStateDto state)> UpdateState(UpdateStateDto updateStateDto) + { + var state = _mapper.Map(updateStateDto); + _dbContext.Entry(state).State = EntityState.Modified; + + try + { + await _dbContext.SaveChangesAsync(); + } + catch (DbUpdateConcurrencyException) + { + if (!await IsStateExists(updateStateDto.Id)) + { + return (false, $"State with id:{updateStateDto.Id} doesn't exist", null)!; + } + + throw; + } + + var dbState = await _dbContext.States.FirstOrDefaultAsync(s => s.Id == state.Id); + + return (true, String.Empty, _mapper.Map(dbState)); + } + + public async Task<(bool isSucceed, string message)> DeleteState(int id) + { + var dbState = await _dbContext.States.FirstOrDefaultAsync(s => s.Id == id); + + if (dbState == null) + { + return (false, $"State with id:{id} doesn't exist"); + } + + _dbContext.States.Remove(dbState); + await _dbContext.SaveChangesAsync(); + + return (true, String.Empty); + } + + public async Task IsStateExists(int id) + { + return await _dbContext.States.AnyAsync(s => s.Id == id); + } +} \ No newline at end of file diff --git a/SharedModels/DataTransferObjects/CountryDto.cs b/SharedModels/DataTransferObjects/CountryDto.cs index cd9f865..7fc36dc 100644 --- a/SharedModels/DataTransferObjects/CountryDto.cs +++ b/SharedModels/DataTransferObjects/CountryDto.cs @@ -6,7 +6,7 @@ public class CountryDto : CreateCountryDto { public int Id { get; set; } - public virtual IList States { get; set; } = null!; + public virtual IList States { get; set; } = null!; } public class CreateCountryDto @@ -24,4 +24,10 @@ public class UpdateCountryDto : CreateCountryDto { [Required] public int Id { get; set; } +} + +public class InStateCountryDto +{ + public string Code { get; set; } = null!; + public string Name { get; set; } = null!; } \ No newline at end of file diff --git a/SharedModels/DataTransferObjects/StateDto.cs b/SharedModels/DataTransferObjects/StateDto.cs index a8d6cc2..16254ec 100644 --- a/SharedModels/DataTransferObjects/StateDto.cs +++ b/SharedModels/DataTransferObjects/StateDto.cs @@ -5,7 +5,7 @@ namespace SharedModels.DataTransferObjects; public class StateDto : CreateStateDto { public int Id { get; set; } - public CountryDto Country { get; set; } = null!; + public InStateCountryDto Country { get; set; } = null!; public virtual IList Cities { get; set; } = null!; } @@ -18,4 +18,16 @@ public class CreateStateDto [Required] public int CountryId { get; set; } +} + +public class UpdateStateDto : CreateStateDto +{ + [Required] + public int Id { get; set; } +} + +public class InCountryStateDto +{ + public int Id { get; set; } + public string Name { get; set; } = null!; } \ No newline at end of file diff --git a/SharedModels/QueryStringParameters/CountryParameters.cs b/SharedModels/QueryStringParameters/CountryParameters.cs index 24e53e3..d1e64c1 100644 --- a/SharedModels/QueryStringParameters/CountryParameters.cs +++ b/SharedModels/QueryStringParameters/CountryParameters.cs @@ -2,10 +2,12 @@ namespace SharedModels.QueryStringParameters; public class CountryParameters : QueryStringParameters { + public const string DefaultFields = "id,code,name"; + public CountryParameters() { Sort = "id"; - Fields = "id,code,name,states"; + Fields = DefaultFields; } public string? Code { get; set; } diff --git a/SharedModels/QueryStringParameters/StateParameters.cs b/SharedModels/QueryStringParameters/StateParameters.cs new file mode 100644 index 0000000..9c44b31 --- /dev/null +++ b/SharedModels/QueryStringParameters/StateParameters.cs @@ -0,0 +1,15 @@ +namespace SharedModels.QueryStringParameters; + +public class StateParameters : QueryStringParameters +{ + public const string DefaultFields = "id,name,countryId"; + + public StateParameters() + { + Sort = "id"; + Fields = DefaultFields; + } + + public string? Name { get; set; } + public int? CountryId { get; set; } +} \ No newline at end of file