#
Adding Paging to your API Queries
#
START FROM THE END OF THE WORKSHOP FINAL PROJECT
Documenting your API with OpenAPI
#
Add Microsoft.EntityFrameworkCore NUGET PACKAGE TO DOMAIN PROJECT
dos
dotnet add package Microsoft.EntityFrameworkCore
#
ADD PAGEDLIST CLASS TO YOUR DOMAIN PROJECT
public class PagedList<T> : List<T>
{
public int CurrentPage { get; }
public int TotalPages { get; }
public int PageSize { get; }
public int TotalCount { get; }
public bool HasPrevious => CurrentPage > 1;
public bool HasNext => CurrentPage < TotalPages;
public PagedList(List<T> items, int count, int pageNumber, int pageSize)
{
TotalCount = count;
PageSize = pageSize;
CurrentPage = pageNumber;
TotalPages = (int)Math.Ceiling(count / (double)pageSize);
AddRange(items);
}
public PagedList(PagedList<T> items)
{
TotalCount = items.TotalCount;
PageSize = items.PageSize;
CurrentPage = items.CurrentPage;
TotalPages = (int)Math.Ceiling(items.TotalCount / (double)items.PageSize);
AddRange(items);
}
public static async Task<PagedList<T>> ToPagedListAsync(IQueryable<T> source, int pageNumber, int pageSize)
{
var count = source.Count();
var items = await source.Skip((pageNumber - 1) * pageSize).Take(pageSize).ToListAsync();
return new PagedList<T>(items, count, pageNumber, pageSize);
}
}
#
UPDATE DATA REPOSITORY INTERFACES IN DOMAIN PROJECT
public interface IAlbumRepository : IDisposable
{
Task<PagedList<Album>> GetAll(int pageNumber, int pageSize);
Task<Album> GetById(int id);
Task<PagedList<Album>> GetByArtistId(int id, int pageNumber, int pageSize);
Task<Album> Add(Album newAlbum);
Task<bool> Update(Album album);
Task<bool> Delete(int id);
}
#
UPDATE DATA REPOSITORIES IN DATA PROJECT
public class AlbumRepository : IAlbumRepository
{
private readonly ChinookContext _context;
public AlbumRepository(ChinookContext context)
{
_context = context;
}
private async Task<bool> AlbumExists(int id) =>
await _context.Albums.AnyAsync(a => a.Id == id);
public void Dispose() => _context.Dispose();
public async Task<PagedList<Album>> GetAll(int pageNumber, int pageSize) =>
await PagedList<Album>.ToPagedListAsync(_context.Set<Album>().AsNoTrackingWithIdentityResolution(),
pageNumber,
pageSize);
public async Task<Album> GetById(int id)
{
var dbAlbum = await _context.Albums.FindAsync(id);
return dbAlbum;
}
public async Task<Album> Add(Album newAlbum)
{
await _context.Albums.AddAsync(newAlbum);
await _context.SaveChangesAsync();
return newAlbum;
}
public async Task<bool> Update(Album album)
{
if (!await AlbumExists(album.Id))
return false;
_context.Albums.Update(album);
await _context.SaveChangesAsync();
return true;
}
public async Task<bool> Delete(int id)
{
if (!await AlbumExists(id))
return false;
var toRemove = await _context.Albums.FindAsync(id);
_context.Albums.Remove(toRemove);
await _context.SaveChangesAsync();
return true;
}
public async Task<PagedList<Album>> GetByArtistId(int id, int pageNumber, int pageSize) =>
await PagedList<Album>.ToPagedListAsync(_context.Albums.Where(a => a.ArtistId == id)
.AsNoTrackingWithIdentityResolution(),
pageNumber,
pageSize);
}
#
UPDATE ICHINOOKSUPERVISOR IN DOMAIN PROJECT
public interface IChinookSupervisor
{
Task<PagedList<AlbumApiModel>> GetAllAlbum(int pageNumber, int pageSize);
Task<AlbumApiModel?> GetAlbumById(int id);
Task<PagedList<AlbumApiModel>> GetAlbumByArtistId(int id, int pageNumber, int pageSize);
Task<AlbumApiModel> AddAlbum(AlbumApiModel newAlbumApiModel);
Task<bool> UpdateAlbum(AlbumApiModel albumApiModel);
Task<bool> DeleteAlbum(int id);
Task<PagedList<ArtistApiModel>> GetAllArtist(int pageNumber, int pageSize);
Task<ArtistApiModel> GetArtistById(int id);
Task<ArtistApiModel> AddArtist(ArtistApiModel newArtistApiModel);
Task<bool> UpdateArtist(ArtistApiModel artistApiModel);
Task<bool> DeleteArtist(int id);
Task<PagedList<CustomerApiModel>> GetAllCustomer(int pageNumber, int pageSize);
Task<CustomerApiModel> GetCustomerById(int id);
Task<PagedList<CustomerApiModel>> GetCustomerBySupportRepId(int id, int pageNumber, int pageSize);
Task<CustomerApiModel> AddCustomer(CustomerApiModel newCustomerApiModel);
Task<bool> UpdateCustomer(CustomerApiModel customerApiModel);
Task<bool> DeleteCustomer(int id);
Task<PagedList<EmployeeApiModel>> GetAllEmployee(int pageNumber, int pageSize);
Task<EmployeeApiModel?> GetEmployeeById(int id);
Task<EmployeeApiModel?> GetEmployeeReportsTo(int id);
Task<EmployeeApiModel> AddEmployee(EmployeeApiModel newEmployeeApiModel);
Task<bool> UpdateEmployee(EmployeeApiModel employeeApiModel);
Task<bool> DeleteEmployee(int id);
Task<IEnumerable<EmployeeApiModel>> GetEmployeeDirectReports(int id);
Task<IEnumerable<EmployeeApiModel>> GetDirectReports(int id);
Task<PagedList<GenreApiModel>> GetAllGenre(int pageNumber, int pageSize);
Task<GenreApiModel?> GetGenreById(int id);
Task<GenreApiModel> AddGenre(GenreApiModel newGenreApiModel);
Task<bool> UpdateGenre(GenreApiModel genreApiModel);
Task<bool> DeleteGenre(int id);
Task<PagedList<InvoiceLineApiModel>> GetAllInvoiceLine(int pageNumber, int pageSize);
Task<InvoiceLineApiModel> GetInvoiceLineById(int id);
Task<PagedList<InvoiceLineApiModel>> GetInvoiceLineByInvoiceId(int id, int pageNumber, int pageSize);
Task<PagedList<InvoiceLineApiModel>> GetInvoiceLineByTrackId(int id, int pageNumber, int pageSize);
Task<InvoiceLineApiModel> AddInvoiceLine(InvoiceLineApiModel newInvoiceLineApiModel);
Task<bool> UpdateInvoiceLine(InvoiceLineApiModel invoiceLineApiModel);
Task<bool> DeleteInvoiceLine(int id);
Task<PagedList<InvoiceApiModel>> GetAllInvoice(int pageNumber, int pageSize);
Task<InvoiceApiModel?> GetInvoiceById(int id);
Task<PagedList<InvoiceApiModel>> GetInvoiceByCustomerId(int id, int pageNumber, int pageSize);
Task<InvoiceApiModel> AddInvoice(InvoiceApiModel newInvoiceApiModel);
Task<bool> UpdateInvoice(InvoiceApiModel invoiceApiModel);
Task<bool> DeleteInvoice(int id);
Task<PagedList<InvoiceApiModel>> GetInvoiceByEmployeeId(int id, int pageNumber, int pageSize);
Task<PagedList<MediaTypeApiModel>> GetAllMediaType(int pageNumber, int pageSize);
Task<MediaTypeApiModel?> GetMediaTypeById(int id);
Task<MediaTypeApiModel> AddMediaType(MediaTypeApiModel newMediaTypeApiModel);
Task<bool> UpdateMediaType(MediaTypeApiModel mediaTypeApiModel);
Task<bool> DeleteMediaType(int id);
Task<PagedList<PlaylistApiModel>> GetAllPlaylist(int pageNumber, int pageSize);
Task<PlaylistApiModel> GetPlaylistById(int id);
Task<PlaylistApiModel> AddPlaylist(PlaylistApiModel newPlaylistApiModel);
Task<bool> UpdatePlaylist(PlaylistApiModel playlistApiModel);
Task<bool> DeletePlaylist(int id);
Task<PagedList<TrackApiModel>> GetAllTrack(int pageNumber, int pageSize);
Task<TrackApiModel?> GetTrackById(int id);
Task<PagedList<TrackApiModel>?> GetTrackByAlbumId(int id, int pageNumber, int pageSize);
Task<PagedList<TrackApiModel>> GetTrackByGenreId(int id, int pageNumber, int pageSize);
Task<PagedList<TrackApiModel>> GetTrackByMediaTypeId(int id, int pageNumber, int pageSize);
Task<PagedList<TrackApiModel>> GetTrackByPlaylistId(int id, int pageNumber, int pageSize);
Task<TrackApiModel> AddTrack(TrackApiModel newTrackApiModel);
Task<bool> UpdateTrack(TrackApiModel trackApiModel);
Task<bool> DeleteTrack(int id);
Task<PagedList<TrackApiModel>> GetTrackByArtistId(int id, int pageNumber, int pageSize);
Task<PagedList<TrackApiModel>> GetTrackByInvoiceId(int id, int pageNumber, int pageSize);
}
#
UPDATE CHINOOKSUPERVISOR PARTIAL CLASS FILES IN DOMAIN PROJECT
public partial class ChinookSupervisor
{
public async Task<PagedList<AlbumApiModel>> GetAllAlbum(int pageNumber, int pageSize) // todo
{
var albums = await _albumRepository.GetAll(pageNumber, pageSize);
var albumApiModels = albums.ConvertAll<AlbumApiModel>();
foreach (var album in albumApiModels)
{
var cacheEntryOptions =
new MemoryCacheEntryOptions().SetSlidingExpiration(TimeSpan.FromSeconds(604800))
.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(604800);
;
_cache.Set(string.Concat("Album-", album.Id), album, (TimeSpan)cacheEntryOptions);
}
var newPagedList = new PagedList<AlbumApiModel>(albumApiModels.ToList(), albums.TotalCount, albums.CurrentPage, albums.PageSize);
return newPagedList;
}
public async Task<AlbumApiModel?> GetAlbumById(int id)
{
var albumApiModelCached = _cache.Get<AlbumApiModel>(string.Concat("Album-", id));
if (albumApiModelCached != null)
{
return albumApiModelCached;
}
else
{
var album = await _albumRepository.GetById(id);
if (album == null) return null;
var albumApiModel = album.Convert();
var result = (_artistRepository.GetById(album.ArtistId)).Result;
if (result != null)
albumApiModel.ArtistName = result.Name;
//albumApiModel.Tracks = (await GetTrackByAlbumId(id) ?? Array.Empty<TrackApiModel>()).ToList();
var cacheEntryOptions =
new MemoryCacheEntryOptions().SetSlidingExpiration(TimeSpan.FromSeconds(604800))
.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(604800);
;
_cache.Set(string.Concat("Album-", albumApiModel.Id), albumApiModel, (TimeSpan)cacheEntryOptions);
return albumApiModel;
}
}
public async Task<PagedList<AlbumApiModel>> GetAlbumByArtistId(int id, int pageNumber, int pageSize)
{
var albums = await _albumRepository.GetByArtistId(id, pageNumber, pageSize);
var albumApiModels = albums.ConvertAll<AlbumApiModel>();
var newPagedList = new PagedList<AlbumApiModel>(albumApiModels.ToList(), albums.TotalCount, albums.CurrentPage, albums.PageSize);
return newPagedList;
}
public async Task<AlbumApiModel> AddAlbum(AlbumApiModel newAlbumApiModel)
{
await _albumValidator.ValidateAndThrowAsync(newAlbumApiModel);
var album = newAlbumApiModel.Convert();
album = await _albumRepository.Add(album);
newAlbumApiModel.Id = album.Id;
return newAlbumApiModel;
}
public async Task<bool> UpdateAlbum(AlbumApiModel albumApiModel)
{
await _albumValidator.ValidateAndThrowAsync(albumApiModel);
var album = await _albumRepository.GetById(albumApiModel.Id);
if (album is null) return false;
album.Id = albumApiModel.Id;
album.Title = albumApiModel.Title;
album.ArtistId = albumApiModel.ArtistId;
return await _albumRepository.Update(album);
}
public Task<bool> DeleteAlbum(int id)
=> _albumRepository.Delete(id);
}
#
UPDATE CONTROLLERS IN API PROJECT
//[Authorize]
[Route("api/[controller]")]
[ApiController]
[EnableCors("CorsPolicy")]
[ApiVersion("1.0")]
public class AlbumController : ControllerBase
{
private readonly IChinookSupervisor _chinookSupervisor;
private readonly ILogger<AlbumController> _logger;
public AlbumController(IChinookSupervisor chinookSupervisor, ILogger<AlbumController> logger)
{
_chinookSupervisor = chinookSupervisor;
_logger = logger;
}
[HttpGet]
[Produces(typeof(List<AlbumApiModel>))]
public async Task<ActionResult<PagedList<AlbumApiModel>>> Get([FromQuery] int pageNumber, [FromQuery] int pageSize)
{
try
{
var albums = await _chinookSupervisor.GetAllAlbum(pageNumber, pageSize);
if (albums.Any())
{
var metadata = new
{
albums.TotalCount,
albums.PageSize,
albums.CurrentPage,
albums.TotalPages,
albums.HasNext,
albums.HasPrevious
};
Response.Headers.Add("X-Pagination", JsonSerializer.Serialize(metadata));
return Ok(albums);
}
var problemDetails = new AlbumProblemDetails
{
Status = StatusCodes.Status404NotFound,
Type = "https://example.com/api/Artist/not-found",
Title = "Could not find any artists",
Detail = "Something went wrong inside the ArtistController Get All action",
AlbumId = null,
Instance = HttpContext.Request.Path
};
_logger.LogError($"{problemDetails.Detail}");
return new ObjectResult(problemDetails)
{
ContentTypes = { "application/problem+json" },
StatusCode = 404,
};
}
catch (AlbumProblemException ex)
{
var problemDetails = new AlbumProblemDetails
{
Status = StatusCodes.Status404NotFound,
Type = "https://example.com/api/Artist/not-found",
Title = "Could not find any artists",
Detail = "Something went wrong inside the ArtistController Get All action",
AlbumId = null,
Instance = HttpContext.Request.Path
};
_logger.LogError($"{problemDetails.Detail}: {ex}");
return new ObjectResult(problemDetails)
{
ContentTypes = { "application/problem+json" },
StatusCode = 403,
};
}
catch (Exception ex)
{
var problemDetails = new AlbumProblemDetails
{
Status = StatusCodes.Status500InternalServerError,
Type = "https://example.com/api/Artist/not-found",
Title = "Could not find any artists",
Detail = "Something went wrong inside the ArtistController Get All action",
AlbumId = null,
Instance = HttpContext.Request.Path
};
_logger.LogError($"Something went wrong inside the AlbumController Get All Album action: {ex}");
return new ObjectResult(problemDetails)
{
ContentTypes = { "application/problem+json" },
StatusCode = 403,
};
}
}
[HttpGet("{id}", Name = "GetAlbumById")]
public async Task<ActionResult<AlbumApiModel>> Get(int id)
{
try
{
var album = await _chinookSupervisor.GetAlbumById(id);
if (album != null)
{
return Ok(album);
}
return StatusCode((int)HttpStatusCode.NotFound, "Album Not Found");
}
catch (AlbumProblemException ex)
{
var problemDetails = new AlbumProblemDetails
{
Status = StatusCodes.Status404NotFound,
Type = "https://example.com/api/Artist/not-found",
Title = "Could not find any artists",
Detail = "Something went wrong inside the ArtistController Get All action",
AlbumId = null,
Instance = HttpContext.Request.Path
};
_logger.LogError($"{problemDetails.Detail}: {ex}");
return new ObjectResult(problemDetails)
{
ContentTypes = { "application/problem+json" },
StatusCode = 403,
};
}
}
[HttpPost]
[Produces("application/json")]
[Consumes("application/json")]
public async Task<ActionResult<AlbumApiModel>> Post([FromBody] AlbumApiModel input)
{
try
{
if (input == null)
{
return StatusCode((int)HttpStatusCode.BadRequest, "Given Album is null");
}
return Ok(await _chinookSupervisor.AddAlbum(input));
}
catch (ValidationException ex)
{
var problemDetails = new AlbumProblemDetails
{
Status = StatusCodes.Status404NotFound,
Type = "https://example.com/api/Artist/not-found",
Title = "Could not find any artists",
Detail = "Something went wrong inside the ArtistController Get All action",
AlbumId = null,
Instance = HttpContext.Request.Path
};
_logger.LogError($"Something went wrong inside the AlbumController Add Album action: {ex}");
return new ObjectResult(problemDetails)
{
ContentTypes = { "application/problem+json" },
StatusCode = 403,
};
}
catch (AlbumProblemException ex)
{
var problemDetails = new AlbumProblemDetails
{
Status = StatusCodes.Status404NotFound,
Type = "https://example.com/api/Artist/not-found",
Title = "Could not find any artists",
Detail = "Something went wrong inside the ArtistController Get All action",
AlbumId = null,
Instance = HttpContext.Request.Path
};
_logger.LogError($"{problemDetails.Detail}: {ex}");
return new ObjectResult(problemDetails)
{
ContentTypes = { "application/problem+json" },
StatusCode = 403,
};
}
catch (Exception ex)
{
var problemDetails = new AlbumProblemDetails
{
Status = StatusCodes.Status500InternalServerError,
Type = "https://example.com/api/Artist/not-found",
Title = "Could not find any artists",
Detail = "Something went wrong inside the ArtistController Get All action",
AlbumId = null,
Instance = HttpContext.Request.Path
};
_logger.LogError($"Something went wrong inside the AlbumController Add Album action: {ex}");
return new ObjectResult(problemDetails)
{
ContentTypes = { "application/problem+json" },
StatusCode = 403,
};
}
}
[HttpPut("{id}")]
[Produces("application/json")]
[Consumes("application/json")]
public async Task<ActionResult<AlbumApiModel>> Put(int id, [FromBody] AlbumApiModel input)
{
try
{
if (input == null)
{
return StatusCode((int)HttpStatusCode.BadRequest, "Given Album is null");
}
return Ok(await _chinookSupervisor.UpdateAlbum(input));
}
catch (ValidationException ex)
{
var problemDetails = new AlbumProblemDetails
{
Status = StatusCodes.Status404NotFound,
Type = "https://example.com/api/Artist/not-found",
Title = "Could not find any artists",
Detail = "Something went wrong inside the ArtistController Get All action",
AlbumId = null,
Instance = HttpContext.Request.Path
};
_logger.LogError($"Something went wrong inside the AlbumController Update Album action: {ex}");
return new ObjectResult(problemDetails)
{
ContentTypes = { "application/problem+json" },
StatusCode = 403,
};
}
catch (Exception ex)
{
var problemDetails = new AlbumProblemDetails
{
Status = StatusCodes.Status404NotFound,
Type = "https://example.com/api/Artist/not-found",
Title = "Could not find any artists",
Detail = "Something went wrong inside the ArtistController Get All action",
AlbumId = null,
Instance = HttpContext.Request.Path
};
_logger.LogError($"Something went wrong inside the AlbumController Update Album action: {ex}");
return new ObjectResult(problemDetails)
{
ContentTypes = { "application/problem+json" },
StatusCode = 403,
};
}
}
[HttpDelete("{id}")]
public async Task<ActionResult> Delete(int id)
{
try
{
return Ok(await _chinookSupervisor.DeleteAlbum(id));
}
catch (Exception ex)
{
var problemDetails = new AlbumProblemDetails
{
Status = StatusCodes.Status404NotFound,
Type = "https://example.com/api/Artist/not-found",
Title = "Could not find any artists",
Detail = "Something went wrong inside the ArtistController Get All action",
AlbumId = null,
Instance = HttpContext.Request.Path
};
_logger.LogError($"Something went wrong inside the AlbumController Delete action: {ex}");
return new ObjectResult(problemDetails)
{
ContentTypes = { "application/problem+json" },
StatusCode = 403,
};
}
}
[HttpGet("artist/{id}")]
public async Task<ActionResult<PagedList<AlbumApiModel>>> GetByArtistId(int id, [FromQuery] int pageNumber, [FromQuery] int pageSize)
{
try
{
var albums = await _chinookSupervisor.GetAlbumByArtistId(id, pageNumber, pageSize);
if (albums.Any())
{
var metadata = new
{
albums.TotalCount,
albums.PageSize,
albums.CurrentPage,
albums.TotalPages,
albums.HasNext,
albums.HasPrevious
};
Response.Headers.Add("X-Pagination", JsonSerializer.Serialize(metadata));
return Ok(albums);
}
return StatusCode((int)HttpStatusCode.NotFound, "No Albums Could Be Found for the Artist");
}
catch (Exception ex)
{
var problemDetails = new AlbumProblemDetails
{
Status = StatusCodes.Status404NotFound,
Type = "https://example.com/api/Artist/not-found",
Title = "Could not find any artists",
Detail = "Something went wrong inside the ArtistController Get All action",
AlbumId = null,
Instance = HttpContext.Request.Path
};
_logger.LogError($"Something went wrong inside the AlbumController Get By Artist action: {ex}");
return new ObjectResult(problemDetails)
{
ContentTypes = { "application/problem+json" },
StatusCode = 403,
};
}
}
}