feat: add state management controller & service

This commit is contained in:
cuqmbr 2022-11-04 17:58:37 +02:00
parent 0929200fdb
commit cafc82fe5b
11 changed files with 376 additions and 21 deletions

View File

@ -1,3 +1,4 @@
using System.Dynamic;
using AutoMapper;
using Server.Models;
using SharedModels.DataTransferObjects;
@ -12,9 +13,12 @@ public class MapperInitializer : Profile
CreateMap<Country, CountryDto>().ReverseMap();
CreateMap<Country, CreateCountryDto>().ReverseMap();
CreateMap<Country, UpdateCountryDto>().ReverseMap();
CreateMap<Country, InStateCountryDto>().ReverseMap();
CreateMap<State, StateDto>().ReverseMap();
CreateMap<State, CreateStateDto>().ReverseMap();
CreateMap<State, UpdateStateDto>().ReverseMap();
CreateMap<State, InCountryStateDto>().ReverseMap();
CreateMap<City, CityDto>().ReverseMap();
CreateMap<City, CreateCityDto>().ReverseMap();

View File

@ -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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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();
}
}

View File

@ -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<ICountryManagementService, CountryManagementService>();
builder.Services.AddScoped<IStateManagementService, StateManagementService>();
builder.Services.AddScoped<IDateTimeService, DateTimeService>();
builder.Services.AddScoped<ISortHelper<Country>, SortHelper<Country>>();
builder.Services.AddScoped<ISortHelper<State>, SortHelper<State>>();
builder.Services.AddScoped<IDataShaper<Country>, DataShaper<Country>>();
builder.Services.AddScoped<IDataShaper<State>, DataShaper<State>>();
// Adding DB Context with PostgreSQL
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");

View File

@ -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<CountryDto>(country));
}
public async Task<(bool isSucceed, string message, IEnumerable<ExpandoObject> countries,
public async Task<(bool isSucceed, string message, IEnumerable<CountryDto> countries,
PagingMetadata<Country> 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<CountryDto>(d));
return (true, "", shapedCountiesData, pagingMetadata);
return (true, "", countryDtos, pagingMetadata);
void SearchByAllCountryFields(ref IQueryable<Country> 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<Country> countries,
@ -110,29 +109,37 @@ public class CountryManagementService : ICountryManagementService
int pageNumber, int pageSize)
{
var metadata = new PagingMetadata<Country>(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<CountryDto>(shapedCountryData);
return (true, "", shapedCountryData);
return (true, "", countryDto);
}
public async Task<(bool isSucceed, string message, UpdateCountryDto country)> UpdateCountry(UpdateCountryDto updateCountryDto)

View File

@ -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<ExpandoObject> countries,
Task<(bool isSucceed, string message, IEnumerable<CountryDto> countries,
PagingMetadata<Country> 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<bool> IsCountryExists(int id);

View File

@ -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<StateDto> states,
PagingMetadata<State> 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<bool> IsStateExists(int id);
}

View File

@ -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<State> _stateSortHelper;
private readonly IDataShaper<State> _stateDataShaper;
public StateManagementService(ApplicationDbContext dbContext,
IMapper mapper, ISortHelper<State> stateSortHelper,
IDataShaper<State> 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<State>(createStateDto);
await _dbContext.States.AddAsync(state);
await _dbContext.SaveChangesAsync();
return (true, String.Empty, _mapper.Map<StateDto>(state));
}
public async Task<(bool isSucceed, string message, IEnumerable<StateDto> states,
PagingMetadata<State> 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<StateDto>(s));
return (true, "", stateDtos, pagingMetadata);
void SearchByAllStateFields(ref IQueryable<State> states,
string? search)
{
if (!states.Any() || String.IsNullOrWhiteSpace(search))
{
return;
}
states = states.Where(s =>
s.Name.ToLower().Contains(search.ToLower()));
}
void SearchByStateCountryId(ref IQueryable<State> 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<State> states,
string? stateName)
{
if (!states.Any() || String.IsNullOrWhiteSpace(stateName))
{
return;
}
states = states.Where(s =>
s.Name.ToLower().Contains(stateName.Trim().ToLower()));
}
PagingMetadata<State> ApplyPaging(ref IQueryable<State> states,
int pageNumber, int pageSize)
{
var metadata = new PagingMetadata<State>(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<StateDto>(shapedStateData);
return (true, "", stateDto);
}
public async Task<(bool isSucceed, string message, UpdateStateDto state)> UpdateState(UpdateStateDto updateStateDto)
{
var state = _mapper.Map<State>(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<UpdateStateDto>(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<bool> IsStateExists(int id)
{
return await _dbContext.States.AnyAsync(s => s.Id == id);
}
}

View File

@ -6,7 +6,7 @@ public class CountryDto : CreateCountryDto
{
public int Id { get; set; }
public virtual IList<StateDto> States { get; set; } = null!;
public virtual IList<InCountryStateDto> 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!;
}

View File

@ -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<CityDto> 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!;
}

View File

@ -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; }

View File

@ -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; }
}