diff --git a/EstusShots.Client/EstusShotsClient.cs b/EstusShots.Client/EstusShotsClient.cs index d191372..f5f3927 100644 --- a/EstusShots.Client/EstusShotsClient.cs +++ b/EstusShots.Client/EstusShotsClient.cs @@ -21,6 +21,7 @@ namespace EstusShots.Client // API Routes public Seasons Seasons { get; } + public Episodes Episodes { get; } /// /// Creates a new instance of @@ -32,6 +33,7 @@ namespace EstusShots.Client HttpClient = new HttpClient {Timeout = TimeSpan.FromSeconds(10)}; Seasons = new Seasons(this); + Episodes = new Episodes(this); } /// diff --git a/EstusShots.Client/Routes/Episodes.cs b/EstusShots.Client/Routes/Episodes.cs new file mode 100644 index 0000000..d64d3e8 --- /dev/null +++ b/EstusShots.Client/Routes/Episodes.cs @@ -0,0 +1,24 @@ +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using EstusShots.Shared.Interfaces; +using EstusShots.Shared.Models; +using EstusShots.Shared.Models.Parameters; + +namespace EstusShots.Client.Routes +{ + public class Episodes : IEpisodesController + { + private readonly EstusShotsClient _client; + + public Episodes(EstusShotsClient client) + { + _client = client; + } + + private string ActionUrl([CallerMemberName]string caller = "") => + $"{_client.ApiUrl}{nameof(Episodes)}/{caller}"; + + public async Task> GetEpisodes(GetEpisodesParameter parameter) + => await _client.PostToApi(ActionUrl(), parameter); + } +} \ No newline at end of file diff --git a/EstusShots.Gtk/MainWindow.cs b/EstusShots.Gtk/MainWindow.cs index 42ea0dc..d875758 100644 --- a/EstusShots.Gtk/MainWindow.cs +++ b/EstusShots.Gtk/MainWindow.cs @@ -55,10 +55,14 @@ namespace EstusShots.Gtk private EstusShotsClient Client { get; } private BindableListControl SeasonsControl { get; } - private void SeasonsViewOnOnSelectionChanged(object sender, SelectionChangedEventArgs e) + private async void SeasonsViewOnOnSelectionChanged(object sender, SelectionChangedEventArgs e) { if (!(e.Selection is Season season)) return; - Info($"Season '{season.DisplayName}' selected"); + + // TODO this is test code + var parameter = new GetEpisodesParameter(season.SeasonId); + var res = await Client.Episodes.GetEpisodes(parameter); + Info($"{season.DisplayName}: {res.Data.Episodes.Count} episodes"); } private async void NewSeasonButtonOnClicked(object sender, EventArgs e) diff --git a/EstusShots.Server/Controllers/EpisodesController.cs b/EstusShots.Server/Controllers/EpisodesController.cs index b81693b..1ed9c1b 100644 --- a/EstusShots.Server/Controllers/EpisodesController.cs +++ b/EstusShots.Server/Controllers/EpisodesController.cs @@ -1,38 +1,39 @@ using System; -using System.Collections.Generic; -using System.Linq; using System.Threading.Tasks; -using AutoMapper; using EstusShots.Server.Services; +using EstusShots.Shared.Interfaces; +using EstusShots.Shared.Models; +using EstusShots.Shared.Models.Parameters; using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -using Dto = EstusShots.Shared.Dto; namespace EstusShots.Server.Controllers { [ApiController] - [Route("/api/[controller]")] - public class EpisodesController : ControllerBase - { - private readonly EstusShotsContext _context; - private readonly IMapper _mapper; + [Route("/api/[controller]/[action]")] + public class EpisodesController : ControllerBase, IEpisodesController + { private readonly ILogger _logger; + private readonly EpisodesService _episodesService; - public EpisodesController(ILogger logger, IMapper mapper, EstusShotsContext context) + public EpisodesController(ILogger logger, EpisodesService episodesService) { _logger = logger; - _mapper = mapper; - _context = context; + _episodesService = episodesService; } - - [HttpGet("seasonId")] - public async Task>> GetEpisodes(Guid seasonId) - { - _logger.LogDebug($"All"); - var episodes = await _context.Episodes.Where(x => x.SeasonId == seasonId).ToListAsync(); - var dtos = _mapper.Map>(episodes); - return dtos; + + public async Task> GetEpisodes(GetEpisodesParameter parameter) + { + try + { + _logger.LogInformation($"Request received from client '{Request.HttpContext.Connection.RemoteIpAddress}'"); + return await _episodesService.GetEpisodes(parameter); + } + catch (Exception e) + { + _logger.LogError(e, "Exception Occured"); + return new ApiResponse(new OperationResult(e)); + } } } } \ No newline at end of file diff --git a/EstusShots.Server/Filters/CustomExceptionFilterAttribute.cs b/EstusShots.Server/Filters/CustomExceptionFilterAttribute.cs new file mode 100644 index 0000000..b48e708 --- /dev/null +++ b/EstusShots.Server/Filters/CustomExceptionFilterAttribute.cs @@ -0,0 +1,21 @@ +using System; +using System.Net; +using EstusShots.Shared.Models; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; + +namespace EstusShots.Server.Filters +{ + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] + public class CustomExceptionFilterAttribute : ExceptionFilterAttribute + { + public override void OnException(ExceptionContext context) + { + var code = HttpStatusCode.InternalServerError; + context.HttpContext.Response.ContentType = "application/json"; + context.HttpContext.Response.StatusCode = (int)code; + var response = new ApiResponse(new OperationResult(false, "Critical Server Error", context.Exception.Message)); + context.Result = new JsonResult(response); + } + } +} \ No newline at end of file diff --git a/EstusShots.Server/Services/EpisodesService.cs b/EstusShots.Server/Services/EpisodesService.cs new file mode 100644 index 0000000..befc30c --- /dev/null +++ b/EstusShots.Server/Services/EpisodesService.cs @@ -0,0 +1,37 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using AutoMapper; +using EstusShots.Shared.Interfaces; +using EstusShots.Shared.Models; +using EstusShots.Shared.Models.Parameters; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using Dto = EstusShots.Shared.Dto; + +namespace EstusShots.Server.Services +{ + public class EpisodesService : IEpisodesController + { + private readonly ILogger _logger; + private readonly EstusShotsContext _context; + private readonly IMapper _mapper; + + public EpisodesService(ILogger logger, EstusShotsContext context, IMapper mapper) + { + _logger = logger; + _context = context; + _mapper = mapper; + } + + public async Task> GetEpisodes(GetEpisodesParameter parameter) + { + var episodes = await _context.Episodes + .Where(x => x.SeasonId == parameter.SeasonId) + .ToListAsync(); + var dtos = _mapper.Map>(episodes); + _logger.LogInformation($"{dtos.Count} episodes loaded for season '{parameter.SeasonId}'"); + return new ApiResponse(new GetEpisodesResponse(dtos)); + } + } +} \ No newline at end of file diff --git a/EstusShots.Server/Startup.cs b/EstusShots.Server/Startup.cs index 5689e89..454ae34 100644 --- a/EstusShots.Server/Startup.cs +++ b/EstusShots.Server/Startup.cs @@ -29,6 +29,7 @@ namespace EstusShots.Server services.AddAutoMapper(typeof(Startup)); services.AddControllers().AddJsonOptions(options => { + options.JsonSerializerOptions.PropertyNameCaseInsensitive = true; if (IsDevelopment) { options.JsonSerializerOptions.WriteIndented = true; @@ -38,7 +39,10 @@ namespace EstusShots.Server { options.SwaggerDoc("v1", new OpenApiInfo { Title = "Estus Shots API", Version = "v1" }); }); + + // Register business logic services services.AddScoped(); + services.AddScoped(); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. @@ -65,6 +69,7 @@ namespace EstusShots.Server app.UseRouting(); app.UseAuthorization(); + app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); } } diff --git a/EstusShots.Shared/Interfaces/IEpisodesController.cs b/EstusShots.Shared/Interfaces/IEpisodesController.cs new file mode 100644 index 0000000..a354628 --- /dev/null +++ b/EstusShots.Shared/Interfaces/IEpisodesController.cs @@ -0,0 +1,19 @@ +using System.Threading.Tasks; +using EstusShots.Shared.Models; +using EstusShots.Shared.Models.Parameters; + +namespace EstusShots.Shared.Interfaces +{ + /// + /// Access to episodes + /// + public interface IEpisodesController + { + /// + /// Load all episodes for a season + /// + /// The parameter object + /// The GetEpisodes response object + Task> GetEpisodes(GetEpisodesParameter parameter); + } +} \ No newline at end of file diff --git a/EstusShots.Shared/Models/OperationResult.cs b/EstusShots.Shared/Models/OperationResult.cs index b6d1c9e..725a9e2 100644 --- a/EstusShots.Shared/Models/OperationResult.cs +++ b/EstusShots.Shared/Models/OperationResult.cs @@ -1,4 +1,5 @@ using System; +using EstusShots.Shared.Interfaces; namespace EstusShots.Shared.Models { @@ -60,4 +61,6 @@ namespace EstusShots.Shared.Models Data = data; } } + + public class CriticalErrorResponse :IApiResponse{} } \ No newline at end of file diff --git a/EstusShots.Shared/Models/Parameters/EpisodeParameters.cs b/EstusShots.Shared/Models/Parameters/EpisodeParameters.cs new file mode 100644 index 0000000..7cc8a6d --- /dev/null +++ b/EstusShots.Shared/Models/Parameters/EpisodeParameters.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using EstusShots.Shared.Dto; +using EstusShots.Shared.Interfaces; + +namespace EstusShots.Shared.Models.Parameters +{ + // GetEpisodes + + /// + /// Parameter class for loading all episodes for a season + /// + public class GetEpisodesParameter : IApiParameter + { + /// + /// ID of the season for which to load the episode list + /// + public Guid SeasonId { get; set; } + + public GetEpisodesParameter(Guid seasonId) + { + SeasonId = seasonId; + } + + public GetEpisodesParameter() + { + SeasonId = Guid.Empty; + } + } + + /// + /// Parameter class returned from the API with all loaded episodes for a season + /// + public class GetEpisodesResponse : IApiResponse + { + /// + /// List of all episodes in the requested season + /// + public List Episodes { get; set; } + + public GetEpisodesResponse(List episodes) + { + Episodes = episodes; + } + + public GetEpisodesResponse() + { + Episodes = new List(); + } + } +} \ No newline at end of file