chore: add CRUD api endpoints for city & address data objects

This commit is contained in:
cuqmbr 2022-11-05 20:26:07 +02:00
parent cafc82fe5b
commit 49064c8f7d
15 changed files with 691 additions and 10 deletions

View File

@ -19,12 +19,18 @@ public class MapperInitializer : Profile
CreateMap<State, CreateStateDto>().ReverseMap();
CreateMap<State, UpdateStateDto>().ReverseMap();
CreateMap<State, InCountryStateDto>().ReverseMap();
CreateMap<State, InCityStateDto>().ReverseMap();
CreateMap<City, CityDto>().ReverseMap();
CreateMap<City, CreateCityDto>().ReverseMap();
CreateMap<City, UpdateCityDto>().ReverseMap();
CreateMap<City, InStateCityDto>().ReverseMap();
CreateMap<City, InAddressCityDto>().ReverseMap();
CreateMap<Address, AddressDto>().ReverseMap();
CreateMap<Address, CreateAddressDto>().ReverseMap();
CreateMap<Address, UpdateAddressDto>().ReverseMap();
CreateMap<Address, InCityAddressDto>().ReverseMap();
CreateMap<RouteAddress, RouteAddressDto>().ReverseMap();
CreateMap<RouteAddress, CreateRouteAddressDto>().ReverseMap();

View File

@ -0,0 +1,101 @@
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
using Server.Services;
using SharedModels.DataTransferObjects;
using SharedModels.QueryStringParameters;
namespace Server.Controllers;
[Route("api/addresses")]
[ApiController]
public class AddressManagementController : ControllerBase
{
private readonly IAddressManagementService _addressManagementService;
public AddressManagementController(IAddressManagementService addressManagementService)
{
_addressManagementService = addressManagementService;
}
[HttpPost]
public async Task<IActionResult> AddAddress(CreateAddressDto address)
{
var result = await _addressManagementService.AddAddress(address);
if (!result.isSucceed)
{
return BadRequest(result.message);
}
return CreatedAtAction(nameof(GetAddress), new {id = result.address.Id}, result.address);
}
[HttpGet]
public async Task<IActionResult> GetAddresses([FromQuery] AddressParameters parameters)
{
var result = await _addressManagementService.GetAddresses(parameters);
if (!result.isSucceed)
{
return BadRequest(result.message);
}
Response.Headers.Add("X-Pagination", JsonConvert.SerializeObject(result.pagingMetadata));
return Ok(result.addresses);
}
[HttpGet("{id}")]
public async Task<IActionResult> GetAddress(int id, [FromQuery] string? fields)
{
if (!await _addressManagementService.IsAddressExists(id))
{
return NotFound();
}
var result = await _addressManagementService.GetAddress(id, fields);
if (!result.isSucceed)
{
return BadRequest(result.message);
}
return Ok(result.address);
}
[HttpPut("{id}")]
public async Task<IActionResult> UpdateRoute(int id, UpdateAddressDto address)
{
if (id != address.Id)
{
return BadRequest();
}
var result = await _addressManagementService.UpdateAddress(address);
if (!result.isSucceed)
{
return BadRequest(result.message);
}
return Ok(result.address);
}
[HttpDelete("{id}")]
public async Task<IActionResult> DeleteRoute(int id)
{
if (!await _addressManagementService.IsAddressExists(id))
{
return NotFound();
}
var result = await _addressManagementService.DeleteAddress(id);
if (!result.isSucceed)
{
return BadRequest(result.message);
}
return NoContent();
}
}

View File

@ -0,0 +1,101 @@
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
using Server.Services;
using SharedModels.DataTransferObjects;
using SharedModels.QueryStringParameters;
namespace Server.Controllers;
[Route("api/cities")]
[ApiController]
public class CityManagementController : ControllerBase
{
private readonly ICityManagementService _cityManagementService;
public CityManagementController(ICityManagementService cityManagementService)
{
_cityManagementService = cityManagementService;
}
[HttpPost]
public async Task<IActionResult> AddCity(CreateCityDto city)
{
var result = await _cityManagementService.AddCity(city);
if (!result.isSucceed)
{
return BadRequest(result.message);
}
return CreatedAtAction(nameof(GetCity), new {id = result.city.Id}, result.city);
}
[HttpGet]
public async Task<IActionResult> GetCities([FromQuery] CityParameters parameters)
{
var result = await _cityManagementService.GetCities(parameters);
if (!result.isSucceed)
{
return BadRequest(result.message);
}
Response.Headers.Add("X-Pagination", JsonConvert.SerializeObject(result.pagingMetadata));
return Ok(result.cities);
}
[HttpGet("{id}")]
public async Task<IActionResult> GetCity(int id, [FromQuery] string? fields)
{
if (!await _cityManagementService.IsCityExists(id))
{
return NotFound();
}
var result = await _cityManagementService.GetCity(id, fields);
if (!result.isSucceed)
{
return BadRequest(result.message);
}
return Ok(result.city);
}
[HttpPut("{id}")]
public async Task<IActionResult> UpdateRoute(int id, UpdateCityDto city)
{
if (id != city.Id)
{
return BadRequest();
}
var result = await _cityManagementService.UpdateCity(city);
if (!result.isSucceed)
{
return BadRequest(result.message);
}
return Ok(result.city);
}
[HttpDelete("{id}")]
public async Task<IActionResult> DeleteRoute(int id)
{
if (!await _cityManagementService.IsCityExists(id))
{
return NotFound();
}
var result = await _cityManagementService.DeleteCity(id);
if (!result.isSucceed)
{
return BadRequest(result.message);
}
return NoContent();
}
}

View File

@ -88,14 +88,20 @@ builder.Services.AddAutoMapper(typeof(MapperInitializer));
builder.Services.AddScoped<ICountryManagementService, CountryManagementService>();
builder.Services.AddScoped<IStateManagementService, StateManagementService>();
builder.Services.AddScoped<ICityManagementService, CityManagementService>();
builder.Services.AddScoped<IAddressManagementService, AddressManagementService>();
builder.Services.AddScoped<IDateTimeService, DateTimeService>();
builder.Services.AddScoped<ISortHelper<Country>, SortHelper<Country>>();
builder.Services.AddScoped<ISortHelper<State>, SortHelper<State>>();
builder.Services.AddScoped<ISortHelper<City>, SortHelper<City>>();
builder.Services.AddScoped<ISortHelper<Address>, SortHelper<Address>>();
builder.Services.AddScoped<IDataShaper<Country>, DataShaper<Country>>();
builder.Services.AddScoped<IDataShaper<State>, DataShaper<State>>();
builder.Services.AddScoped<IDataShaper<City>, DataShaper<City>>();
builder.Services.AddScoped<IDataShaper<Address>, DataShaper<Address>>();
// Adding DB Context with PostgreSQL
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");

View File

@ -0,0 +1,185 @@
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 AddressManagementService : IAddressManagementService
{
private readonly ApplicationDbContext _dbContext;
private readonly IMapper _mapper;
private readonly ISortHelper<Address> _addressSortHelper;
private readonly IDataShaper<Address> _addressDataShaper;
public AddressManagementService(ApplicationDbContext dbContext,
IMapper mapper, ISortHelper<Address> addressSortHelper,
IDataShaper<Address> addressDataShaper)
{
_dbContext = dbContext;
_mapper = mapper;
_addressSortHelper = addressSortHelper;
_addressDataShaper = addressDataShaper;
}
public async Task<(bool isSucceed, string message, AddressDto address)> AddAddress(CreateAddressDto createAddressDto)
{
var address = _mapper.Map<Address>(createAddressDto);
await _dbContext.Addresses.AddAsync(address);
await _dbContext.SaveChangesAsync();
return (true, String.Empty, _mapper.Map<AddressDto>(address));
}
public async Task<(bool isSucceed, string message, IEnumerable<AddressDto> addresses,
PagingMetadata<Address> pagingMetadata)> GetAddresses(AddressParameters parameters)
{
var dbAddresses = _dbContext.Addresses.Include(a => a.City)
.ThenInclude(c => c.State).ThenInclude(s => s.Country)
.AsQueryable();
SearchByAllAddressFields(ref dbAddresses, parameters.Search);
SearchByAddressName(ref dbAddresses, parameters.Name);
SearchByCityId(ref dbAddresses, parameters.CityId);
try
{
dbAddresses = _addressSortHelper.ApplySort(dbAddresses, 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 = dbAddresses.Any();
}
catch (Exception e)
{
return (false, "Invalid sorting string", null, null)!;
}
var pagingMetadata = ApplyPaging(ref dbAddresses, parameters.PageNumber,
parameters.PageSize);
var shapedAddressesData = _addressDataShaper.ShapeData(dbAddresses, parameters.Fields);
var addressDtos = shapedAddressesData.ToList().ConvertAll(a => _mapper.Map<AddressDto>(a));
return (true, "", addressDtos, pagingMetadata);
void SearchByAllAddressFields(ref IQueryable<Address> addresses,
string? search)
{
if (!addresses.Any() || String.IsNullOrWhiteSpace(search))
{
return;
}
addresses = addresses.Where(a =>
a.Name.ToLower().Contains(search.ToLower()));
}
void SearchByCityId(ref IQueryable<Address> addresses,
int? cityId)
{
if (!addresses.Any() || cityId == null)
{
return;
}
addresses = addresses.Where(a => a.CityId == cityId);
}
void SearchByAddressName(ref IQueryable<Address> addresses,
string? addressName)
{
if (!addresses.Any() || String.IsNullOrWhiteSpace(addressName))
{
return;
}
addresses = addresses.Where(a =>
a.Name.ToLower().Contains(addressName.Trim().ToLower()));
}
PagingMetadata<Address> ApplyPaging(ref IQueryable<Address> addresses,
int pageNumber, int pageSize)
{
var metadata = new PagingMetadata<Address>(addresses,
pageNumber, pageSize);
addresses = addresses
.Skip((pageNumber - 1) * pageSize)
.Take(pageSize);
return metadata;
}
}
public async Task<(bool isSucceed, string message, AddressDto address)> GetAddress(int id, string? fields)
{
var dbAddress = await _dbContext.Addresses.Where(a => a.Id == id)
.Include(a => a.City).ThenInclude(c => c.State)
.ThenInclude(s => s.Country)
.FirstOrDefaultAsync();
if (dbAddress == null)
{
return (false, $"Address doesn't exist", null)!;
}
if (String.IsNullOrWhiteSpace(fields))
{
fields = AddressParameters.DefaultFields;
}
var shapedAddressData = _addressDataShaper.ShapeData(dbAddress, fields);
var addressDto = _mapper.Map<AddressDto>(shapedAddressData);
return (true, "", addressDto);
}
public async Task<(bool isSucceed, string message, UpdateAddressDto address)> UpdateAddress(UpdateAddressDto updateAddressDto)
{
var address = _mapper.Map<Address>(updateAddressDto);
_dbContext.Entry(address).State = EntityState.Modified;
try
{
await _dbContext.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!await IsAddressExists(updateAddressDto.Id))
{
return (false, $"Address with id:{updateAddressDto.Id} doesn't exist", null)!;
}
throw;
}
var dbAddress = await _dbContext.Addresses.FirstOrDefaultAsync(a => a.Id == address.Id);
return (true, String.Empty, _mapper.Map<UpdateAddressDto>(dbAddress));
}
public async Task<(bool isSucceed, string message)> DeleteAddress(int id)
{
var dbAddress = await _dbContext.Addresses.FirstOrDefaultAsync(a => a.Id == id);
if (dbAddress == null)
{
return (false, $"Address with id:{id} doesn't exist");
}
_dbContext.Addresses.Remove(dbAddress);
await _dbContext.SaveChangesAsync();
return (true, String.Empty);
}
public async Task<bool> IsAddressExists(int id)
{
return await _dbContext.Addresses.AnyAsync(a => a.Id == id);
}
}

View File

@ -0,0 +1,185 @@
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 CityManagementService : ICityManagementService
{
private readonly ApplicationDbContext _dbContext;
private readonly IMapper _mapper;
private readonly ISortHelper<City> _citySortHelper;
private readonly IDataShaper<City> _cityDataShaper;
public CityManagementService(ApplicationDbContext dbContext,
IMapper mapper, ISortHelper<City> citySortHelper,
IDataShaper<City> cityDataShaper)
{
_dbContext = dbContext;
_mapper = mapper;
_citySortHelper = citySortHelper;
_cityDataShaper = cityDataShaper;
}
public async Task<(bool isSucceed, string message, CityDto city)> AddCity(CreateCityDto createCityDto)
{
var city = _mapper.Map<City>(createCityDto);
await _dbContext.Cities.AddAsync(city);
await _dbContext.SaveChangesAsync();
return (true, String.Empty, _mapper.Map<CityDto>(city));
}
public async Task<(bool isSucceed, string message, IEnumerable<CityDto> cities,
PagingMetadata<City> pagingMetadata)> GetCities(CityParameters parameters)
{
var dbCities = _dbContext.Cities.Include(c => c.State)
.ThenInclude(s => s.Country).Include(c => c.Addresses)
.AsQueryable();
SearchByAllCityFields(ref dbCities, parameters.Search);
SearchByCityName(ref dbCities, parameters.Name);
SearchByStateId(ref dbCities, parameters.StateId);
try
{
dbCities = _citySortHelper.ApplySort(dbCities, 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 = dbCities.Any();
}
catch (Exception e)
{
return (false, "Invalid sorting string", null, null)!;
}
var pagingMetadata = ApplyPaging(ref dbCities, parameters.PageNumber,
parameters.PageSize);
var shapedCitiesData = _cityDataShaper.ShapeData(dbCities, parameters.Fields);
var cityDtos = shapedCitiesData.ToList().ConvertAll(s => _mapper.Map<CityDto>(s));
return (true, "", cityDtos, pagingMetadata);
void SearchByAllCityFields(ref IQueryable<City> cities,
string? search)
{
if (!cities.Any() || String.IsNullOrWhiteSpace(search))
{
return;
}
cities = cities.Where(s =>
s.Name.ToLower().Contains(search.ToLower()));
}
void SearchByStateId(ref IQueryable<City> cities,
int? stateId)
{
if (!cities.Any() || stateId == null)
{
return;
}
cities = cities.Where(s => s.StateId == stateId);
}
void SearchByCityName(ref IQueryable<City> cities,
string? cityName)
{
if (!cities.Any() || String.IsNullOrWhiteSpace(cityName))
{
return;
}
cities = cities.Where(s =>
s.Name.ToLower().Contains(cityName.Trim().ToLower()));
}
PagingMetadata<City> ApplyPaging(ref IQueryable<City> cities,
int pageNumber, int pageSize)
{
var metadata = new PagingMetadata<City>(cities,
pageNumber, pageSize);
cities = cities
.Skip((pageNumber - 1) * pageSize)
.Take(pageSize);
return metadata;
}
}
public async Task<(bool isSucceed, string message, CityDto city)> GetCity(int id, string? fields)
{
var dbCity = await _dbContext.Cities.Where(s => s.Id == id)
.Include(c => c.State).ThenInclude(s => s.Country)
.Include(c => c.Addresses)
.FirstOrDefaultAsync();
if (dbCity == null)
{
return (false, $"City doesn't exist", null)!;
}
if (String.IsNullOrWhiteSpace(fields))
{
fields = CityParameters.DefaultFields;
}
var shapedCityData = _cityDataShaper.ShapeData(dbCity, fields);
var cityDto = _mapper.Map<CityDto>(shapedCityData);
return (true, "", cityDto);
}
public async Task<(bool isSucceed, string message, UpdateCityDto city)> UpdateCity(UpdateCityDto updateCityDto)
{
var city = _mapper.Map<City>(updateCityDto);
_dbContext.Entry(city).State = EntityState.Modified;
try
{
await _dbContext.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!await IsCityExists(updateCityDto.Id))
{
return (false, $"City with id:{updateCityDto.Id} doesn't exist", null)!;
}
throw;
}
var dbCity = await _dbContext.Cities.FirstOrDefaultAsync(s => s.Id == city.Id);
return (true, String.Empty, _mapper.Map<UpdateCityDto>(dbCity));
}
public async Task<(bool isSucceed, string message)> DeleteCity(int id)
{
var dbCity = await _dbContext.Cities.FirstOrDefaultAsync(s => s.Id == id);
if (dbCity == null)
{
return (false, $"City with id:{id} doesn't exist");
}
_dbContext.Cities.Remove(dbCity);
await _dbContext.SaveChangesAsync();
return (true, String.Empty);
}
public async Task<bool> IsCityExists(int id)
{
return await _dbContext.Cities.AnyAsync(s => s.Id == id);
}
}

View File

@ -0,0 +1,17 @@
using Server.Models;
using SharedModels.DataTransferObjects;
using SharedModels.QueryStringParameters;
namespace Server.Services;
public interface IAddressManagementService
{
Task<(bool isSucceed, string message, AddressDto address)> AddAddress(CreateAddressDto createAddressDto);
Task<(bool isSucceed, string message, IEnumerable<AddressDto> addresses,
PagingMetadata<Address> pagingMetadata)> GetAddresses(AddressParameters parameters);
Task<(bool isSucceed, string message, AddressDto address)> GetAddress(int id, string? fields);
Task<(bool isSucceed, string message, UpdateAddressDto address)> UpdateAddress(UpdateAddressDto updateAddressDto);
Task<(bool isSucceed, string message)> DeleteAddress(int id);
Task<bool> IsAddressExists(int id);
}

View File

@ -0,0 +1,17 @@
using Server.Models;
using SharedModels.DataTransferObjects;
using SharedModels.QueryStringParameters;
namespace Server.Services;
public interface ICityManagementService
{
Task<(bool isSucceed, string message, CityDto city)> AddCity(CreateCityDto createCityDto);
Task<(bool isSucceed, string message, IEnumerable<CityDto> cities,
PagingMetadata<City> pagingMetadata)> GetCities(CityParameters parameters);
Task<(bool isSucceed, string message, CityDto city)> GetCity(int id, string? fields);
Task<(bool isSucceed, string message, UpdateCityDto city)> UpdateCity(UpdateCityDto updateCityDto);
Task<(bool isSucceed, string message)> DeleteCity(int id);
Task<bool> IsCityExists(int id);
}

View File

@ -45,7 +45,7 @@ public class StateManagementService : IStateManagementService
SearchByAllStateFields(ref dbStates, parameters.Search);
SearchByStateName(ref dbStates, parameters.Name);
SearchByStateCountryId(ref dbStates, parameters.CountryId);
SearchByCountryId(ref dbStates, parameters.CountryId);
try
{
@ -80,7 +80,7 @@ public class StateManagementService : IStateManagementService
s.Name.ToLower().Contains(search.ToLower()));
}
void SearchByStateCountryId(ref IQueryable<State> states,
void SearchByCountryId(ref IQueryable<State> states,
int? countryId)
{
if (!states.Any() || countryId == null)
@ -88,9 +88,7 @@ public class StateManagementService : IStateManagementService
return;
}
states = states.Where(s =>
s.CountryId.ToString().ToLower()
.Contains(countryId.ToString()!.Trim().ToLower()));
states = states.Where(s => s.CountryId == countryId);
}
void SearchByStateName(ref IQueryable<State> states,

View File

@ -6,7 +6,7 @@ public class AddressDto : CreateAddressDto
{
public int Id { get; set; }
public CityDto City { get; set; } = null!;
public InAddressCityDto City { get; set; } = null!;
public virtual IList<RouteAddressDto> RouteAddresses { get; set; } = null!;
}
@ -27,4 +27,18 @@ public class CreateAddressDto
[Required]
public int CityId { get; set; }
}
public class UpdateAddressDto : CreateAddressDto
{
[Required]
public int Id { get; set; }
}
public class InCityAddressDto
{
public int Id { get; set; }
public string Name { get; set; } = null!;
public double Latitude { get; set; }
public double Longitude { get; set; }
}

View File

@ -6,9 +6,9 @@ public class CityDto : CreateCityDto
{
public int Id { get; set; }
public StateDto State { get; set; } = null!;
public InCityStateDto State { get; set; } = null!;
public virtual IList<AddressDto>? Addresses { get; set; }
public virtual IList<InCityAddressDto>? Addresses { get; set; }
}
public class CreateCityDto
@ -19,4 +19,21 @@ public class CreateCityDto
[Required]
public int StateId { get; set; }
}
public class UpdateCityDto : CreateCityDto
{
[Required]
public int Id { get; set; }
}
public class InStateCityDto
{
public int Id { get; set; }
public string Name { get; set; } = null!;
}
public class InAddressCityDto
{
public string Name { get; set; } = null!;
}

View File

@ -7,7 +7,7 @@ public class StateDto : CreateStateDto
public int Id { get; set; }
public InStateCountryDto Country { get; set; } = null!;
public virtual IList<CityDto> Cities { get; set; } = null!;
public virtual IList<InStateCityDto> Cities { get; set; } = null!;
}
public class CreateStateDto
@ -30,4 +30,9 @@ public class InCountryStateDto
{
public int Id { get; set; }
public string Name { get; set; } = null!;
}
public class InCityStateDto
{
public string Name { get; set; } = null!;
}

View File

@ -0,0 +1,15 @@
namespace SharedModels.QueryStringParameters;
public class AddressParameters : QueryStringParameters
{
public const string DefaultFields = "id,name,cityId";
public AddressParameters()
{
Sort = "id";
Fields = DefaultFields;
}
public string? Name { get; set; }
public int? CityId { get; set; }
}

View File

@ -0,0 +1,15 @@
namespace SharedModels.QueryStringParameters;
public class CityParameters : QueryStringParameters
{
public const string DefaultFields = "id,name,stateId";
public CityParameters()
{
Sort = "id";
Fields = DefaultFields;
}
public string? Name { get; set; }
public int? StateId { get; set; }
}

View File

@ -4,7 +4,6 @@ namespace SharedModels.Requests;
public class RegistrationRequest
{
[Required]
public string Username { get; set; } = null!;
[Required]
public string Email { get; set; } = null!;