diff --git a/.gitignore b/.gitignore index d5fd0f2..53569f9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ bin/ obj/ /packages/ -.blob +*.blob .idea/ \ No newline at end of file diff --git a/EstusShots.Client/EstusShotsClient.cs b/EstusShots.Client/EstusShotsClient.cs index 83af933..d191372 100644 --- a/EstusShots.Client/EstusShotsClient.cs +++ b/EstusShots.Client/EstusShotsClient.cs @@ -1,10 +1,10 @@ using System; -using System.Collections.Generic; using System.Net.Http; using System.Text; using System.Text.Json; using System.Threading.Tasks; -using EstusShots.Shared.Dto; +using EstusShots.Client.Routes; +using EstusShots.Shared.Interfaces; using EstusShots.Shared.Models; namespace EstusShots.Client @@ -13,49 +13,52 @@ namespace EstusShots.Client { private readonly JsonSerializerOptions _serializerOptions = new JsonSerializerOptions { - PropertyNameCaseInsensitive = true, + PropertyNameCaseInsensitive = true }; - - private string ApiUrl { get; } private HttpClient HttpClient { get; } + public string ApiUrl { get; } + + // API Routes + public Seasons Seasons { get; } + + /// + /// Creates a new instance of + /// + /// Base URL of the Estus Shots API host public EstusShotsClient(string apiUrl) { ApiUrl = apiUrl; HttpClient = new HttpClient {Timeout = TimeSpan.FromSeconds(10)}; + + Seasons = new Seasons(this); } - - public async Task<(OperationResult, List)> GetSeasons() + + /// + /// Generic method to post a request to the API + /// + /// URL to the desired action + /// The API parameter object instance + /// API response class that implements + /// API parameter class that implements + /// + public async Task> PostToApi(string url, TParam parameter) + where TParam : IApiParameter, new() + where TResult : class, IApiResponse, new() { try { - var response = await HttpClient.GetAsync(ApiUrl + "seasons"); - var jsonData = await response.Content.ReadAsStringAsync(); - var data = JsonSerializer.Deserialize>(jsonData, _serializerOptions); - return (new OperationResult(), data); + var serialized = JsonSerializer.Serialize(parameter); + var content = new StringContent(serialized, Encoding.UTF8, "application/json"); + var response = await HttpClient.PostAsync(url, content); + response.EnsureSuccessStatusCode(); + var json = await response.Content.ReadAsStringAsync(); + var result = JsonSerializer.Deserialize>(json, _serializerOptions); + return result; } catch (Exception e) { - return (new OperationResult(e), new List()); - } - } - - public async Task<(OperationResult, Guid)> CreateSeason(Season season) - { - try - { - var content = new StringContent(JsonSerializer.Serialize(season), Encoding.UTF8, "application/json"); - var response = await HttpClient.PostAsync(ApiUrl + "season", content); - if (!response.IsSuccessStatusCode) - { - return (new OperationResult(false, response.ReasonPhrase), Guid.Empty); - } - // TODO should give the created id - return (new OperationResult(), Guid.Empty); - } - catch (Exception e) - { - return (new OperationResult(e), Guid.Empty); + return new ApiResponse(new OperationResult(e)); } } diff --git a/EstusShots.Client/Routes/Seasons.cs b/EstusShots.Client/Routes/Seasons.cs new file mode 100644 index 0000000..51be399 --- /dev/null +++ b/EstusShots.Client/Routes/Seasons.cs @@ -0,0 +1,30 @@ +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 Seasons : ISeasonsController + { + private readonly EstusShotsClient _client; + + public Seasons(EstusShotsClient client) + { + _client = client; + } + + private string ActionUrl([CallerMemberName]string caller = "") => + $"{_client.ApiUrl}{nameof(Seasons)}/{caller}"; + + public async Task> GetSeasons(GetSeasonsParameter parameter) => + await _client.PostToApi(ActionUrl(), parameter); + + public async Task> GetSeason(GetSeasonParameter parameter) => + await _client.PostToApi(ActionUrl(), parameter); + + public async Task> SaveSeason(SaveSeasonParameter 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 929536f..42ea0dc 100644 --- a/EstusShots.Gtk/MainWindow.cs +++ b/EstusShots.Gtk/MainWindow.cs @@ -5,6 +5,7 @@ using System.Threading.Tasks; using EstusShots.Client; using EstusShots.Gtk.Controls; using EstusShots.Shared.Dto; +using EstusShots.Shared.Models.Parameters; using Gtk; using UI = Gtk.Builder.ObjectAttribute; @@ -63,6 +64,7 @@ namespace EstusShots.Gtk private async void NewSeasonButtonOnClicked(object sender, EventArgs e) { using var _ = new LoadingMode(this); + // TODO real season edit control var season = new Season { Game = "Test Game", @@ -70,14 +72,13 @@ namespace EstusShots.Gtk Start = DateTime.Now, Description = "This is a demo description!" }; - - var (res, _) = await Client.CreateSeason(season); - if (!res.Success) + var parameter = new SaveSeasonParameter(season); + var res = await Client.Seasons.SaveSeason(parameter); + if (!res.OperationResult.Success) { - _infoLabel.Text = $"Error while creating Season: {res.ShortMessage}"; + _infoLabel.Text = $"Error while creating Season: {res.OperationResult.ShortMessage}"; return; } - await ReloadSeasons(); Info("Created new Season"); } @@ -91,14 +92,15 @@ namespace EstusShots.Gtk private async Task ReloadSeasons() { - var (res, seasons) = await Task.Factory.StartNew(() => Client.GetSeasons().Result); - if (!res.Success) + var res = await Task.Factory.StartNew( + () => Client.Seasons.GetSeasons(new GetSeasonsParameter()).Result); + if (!res.OperationResult.Success) { - _infoLabel.Text = $"Refresh Failed: {res.ShortMessage}"; + _infoLabel.Text = $"Refresh Failed: {res.OperationResult.ShortMessage}"; return; } - SeasonsControl.Items = seasons; + SeasonsControl.Items = res.Data.Seasons; SeasonsControl.DataBind(); Info("List Refreshed"); } diff --git a/EstusShots.Server/Controllers/SeasonController.cs b/EstusShots.Server/Controllers/SeasonController.cs deleted file mode 100644 index 697cc38..0000000 --- a/EstusShots.Server/Controllers/SeasonController.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System; -using System.Threading.Tasks; -using AutoMapper; -using EstusShots.Server.Models; -using EstusShots.Server.Services; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; -using Dto = EstusShots.Shared.Dto; - -namespace EstusShots.Server.Controllers -{ - [ApiController] - [Route("/api/[controller]")] - public class SeasonController : ControllerBase - { - private readonly EstusShotsContext _context; - private readonly IMapper _mapper; - private readonly ILogger _logger; - - public SeasonController(EstusShotsContext context, IMapper mapper, ILogger logger) - { - _context = context; - _mapper = mapper; - _logger = logger; - } - - [HttpGet("{id}")] - public async Task> GetSeason(Guid id) - { - var season = await _context.Seasons.FindAsync(id); - if (season == null) {return NotFound();} - - var seasonDto = _mapper.Map(season); - return seasonDto; - } - - [HttpPost] - public async Task> CreateSeason(Dto.Season season) - { - var dbSeason = _mapper.Map(season); - _context.Seasons.Add(dbSeason); - try - { - await _context.SaveChangesAsync(); - _logger.LogInformation("New season created"); - } - catch (Exception e) - { - _logger.LogError(e, "Error while saving Season"); - } - return CreatedAtAction(nameof(GetSeason), new {id = dbSeason.SeasonId}, dbSeason); - } - } -} \ No newline at end of file diff --git a/EstusShots.Server/Controllers/SeasonsController.cs b/EstusShots.Server/Controllers/SeasonsController.cs index 1932ba7..d7001db 100644 --- a/EstusShots.Server/Controllers/SeasonsController.cs +++ b/EstusShots.Server/Controllers/SeasonsController.cs @@ -1,33 +1,70 @@ -using System.Collections.Generic; +using System; 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 Dto = EstusShots.Shared.Dto; +using Microsoft.Extensions.Logging; namespace EstusShots.Server.Controllers { [ApiController] - [Route("/api/[controller]")] - public class SeasonsController : ControllerBase + [Route("/api/[controller]/[action]")] + public class SeasonsController : ControllerBase, ISeasonsController { - private readonly EstusShotsContext _context; - private readonly IMapper _mapper; + private readonly ILogger _logger; + private readonly SeasonsService _seasonsService; - public SeasonsController(EstusShotsContext context, IMapper mapper) + public SeasonsController(ILogger logger, SeasonsService seasonsService) { - _context = context; - _mapper = mapper; + _seasonsService = seasonsService; + _logger = logger; } - [HttpGet] - public async Task>> GetSeasons() + [HttpPost] + public async Task> GetSeasons(GetSeasonsParameter parameter) { - var seasons = await _context.Seasons.ToListAsync(); - var dtos = _mapper.Map>(seasons); - return dtos; + try + { + _logger.LogInformation($"Request received from client '{Request.HttpContext.Connection.RemoteIpAddress}'"); + return await _seasonsService.GetSeasons(parameter); + } + catch (Exception e) + { + _logger.LogError(e, "Exception Occured"); + return new ApiResponse(new OperationResult(e)); + } + } + + [HttpPost] + public async Task> GetSeason(GetSeasonParameter parameter) + { + try + { + _logger.LogInformation($"Request received from client '{Request.HttpContext.Connection.RemoteIpAddress}'"); + return await _seasonsService.GetSeason(parameter); + } + catch (Exception e) + { + _logger.LogError(e, "Exception Occured"); + return new ApiResponse(new OperationResult(e)); + } + } + + public async Task> SaveSeason(SaveSeasonParameter parameter) + { + try + { + _logger.LogInformation($"Request received from client '{Request.HttpContext.Connection.RemoteIpAddress}'"); + return await _seasonsService.SaveSeason(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/Properties/launchSettings.json b/EstusShots.Server/Properties/launchSettings.json index 7509c02..18bf8c7 100644 --- a/EstusShots.Server/Properties/launchSettings.json +++ b/EstusShots.Server/Properties/launchSettings.json @@ -1,23 +1,7 @@ { "$schema": "http://json.schemastore.org/launchsettings.json", - "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, - "iisExpress": { - "applicationUrl": "http://localhost:26850", - "sslPort": 44318 - } - }, "profiles": { - "IIS Express": { - "commandName": "IISExpress", - "launchBrowser": true, - "launchUrl": "weatherforecast", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - }, - "EstusShots.Server": { + "EstusShots.Server (Dev)": { "commandName": "Project", "launchBrowser": false, "launchUrl": "", @@ -25,6 +9,15 @@ "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } + }, + "EstusShots.Server (Prod)": { + "commandName": "Project", + "launchBrowser": false, + "launchUrl": "", + "applicationUrl": "https://localhost:5001;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Production" + } } } } diff --git a/EstusShots.Server/Services/SeasonsService.cs b/EstusShots.Server/Services/SeasonsService.cs new file mode 100644 index 0000000..efddb7f --- /dev/null +++ b/EstusShots.Server/Services/SeasonsService.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using AutoMapper; +using EstusShots.Server.Models; +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 SeasonsService : ISeasonsController + { + private readonly EstusShotsContext _context; + private readonly IMapper _mapper; + private readonly ILogger _logger; + + public SeasonsService(EstusShotsContext context, IMapper mapper, ILogger logger) + { + _context = context; + _mapper = mapper; + _logger = logger; + } + + public async Task> GetSeasons(GetSeasonsParameter parameter) + { + var seasons = await _context.Seasons.ToListAsync(); + var dtos = _mapper.Map>(seasons); + return new ApiResponse(new GetSeasonsResponse(dtos)); + } + + public async Task> GetSeason(GetSeasonParameter parameter) + { + var season = await _context.Seasons.FindAsync(parameter.SeasonId); + if (season == null) + { + _logger.LogWarning($"Season '{parameter.SeasonId}' not found in database"); + return new ApiResponse(new OperationResult(false, $"Season '{parameter.SeasonId}' not found in database")); + } + var seasonDto = _mapper.Map(season); + return new ApiResponse(new GetSeasonResponse(seasonDto)); + } + + public async Task> SaveSeason(SaveSeasonParameter parameter) + { + var season = _mapper.Map(parameter.Season); + var existing = await _context.Seasons.FindAsync(season.SeasonId); + if (existing == null) + { + _context.Seasons.Add(season);await _context.SaveChangesAsync(); + _logger.LogInformation($"New season created: '{season.SeasonId}'"); + } + else + { + throw new NotImplementedException(); + } + return new ApiResponse(new SaveSeasonResponse(season.SeasonId)); + } + } +} \ No newline at end of file diff --git a/EstusShots.Server/Startup.cs b/EstusShots.Server/Startup.cs index 483c991..41ec82e 100644 --- a/EstusShots.Server/Startup.cs +++ b/EstusShots.Server/Startup.cs @@ -6,37 +6,46 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; +using IHostingEnvironment = Microsoft.AspNetCore.Hosting.IHostingEnvironment; namespace EstusShots.Server { public class Startup { + public IConfiguration Configuration { get; } + + private bool IsDevelopment { get; set; } + public Startup(IConfiguration configuration) { Configuration = configuration; } - public IConfiguration Configuration { get; } - // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { services.AddDbContext( opt => opt.UseInMemoryDatabase("debug.db")); services.AddAutoMapper(typeof(Startup)); - services.AddControllers(); + services.AddControllers().AddJsonOptions(options => + { + if (IsDevelopment) + { + options.JsonSerializerOptions.WriteIndented = true; + } + }); + services.AddScoped(); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { - if (env.IsDevelopment()) + IsDevelopment = env.IsDevelopment(); + if (IsDevelopment) { app.UseDeveloperExceptionPage(); } - - if (!env.IsDevelopment()) + else { app.UseHttpsRedirection(); } diff --git a/EstusShots.Shared/Dto/Season.cs b/EstusShots.Shared/Dto/Season.cs index 9323d14..76fcadc 100644 --- a/EstusShots.Shared/Dto/Season.cs +++ b/EstusShots.Shared/Dto/Season.cs @@ -8,9 +8,9 @@ namespace EstusShots.Shared.Dto public int Number { get; set; } - public string Game { get; set; } = default!; + public string Game { get; set; } - public string? Description { get; set; } + public string Description { get; set; } public DateTime Start { get; set; } diff --git a/EstusShots.Shared/EstusShots.Shared.csproj b/EstusShots.Shared/EstusShots.Shared.csproj index 67ec3d9..e9178a1 100644 --- a/EstusShots.Shared/EstusShots.Shared.csproj +++ b/EstusShots.Shared/EstusShots.Shared.csproj @@ -1,8 +1,8 @@ - netcoreapp3.1 disable + netcoreapp3.1 diff --git a/EstusShots.Shared/Interfaces/IApiParameter.cs b/EstusShots.Shared/Interfaces/IApiParameter.cs new file mode 100644 index 0000000..c990fec --- /dev/null +++ b/EstusShots.Shared/Interfaces/IApiParameter.cs @@ -0,0 +1,12 @@ +namespace EstusShots.Shared.Interfaces +{ + /// + /// Marks a class that can be used as a parameter for API requests + /// + public interface IApiParameter { } + + /// + /// Marks a class as response type that is returned from API requests + /// + public interface IApiResponse { } +} \ No newline at end of file diff --git a/EstusShots.Shared/Interfaces/ISeasonsController.cs b/EstusShots.Shared/Interfaces/ISeasonsController.cs new file mode 100644 index 0000000..b8e7ef9 --- /dev/null +++ b/EstusShots.Shared/Interfaces/ISeasonsController.cs @@ -0,0 +1,23 @@ +using System.Threading.Tasks; +using EstusShots.Shared.Models; +using EstusShots.Shared.Models.Parameters; + +namespace EstusShots.Shared.Interfaces +{ + /// + /// Access many seasons with one request + /// + public interface ISeasonsController + { + /// + /// Get a list of all seasons in the system + /// + /// + /// + Task> GetSeasons(GetSeasonsParameter parameter); + + Task> GetSeason(GetSeasonParameter parameter); + + Task> SaveSeason(SaveSeasonParameter parameter); + } +} \ No newline at end of file diff --git a/EstusShots.Shared/Models/OperationResult.cs b/EstusShots.Shared/Models/OperationResult.cs index 18a9a77..b6d1c9e 100644 --- a/EstusShots.Shared/Models/OperationResult.cs +++ b/EstusShots.Shared/Models/OperationResult.cs @@ -29,4 +29,35 @@ namespace EstusShots.Shared.Models StackTrace = e.StackTrace; } } + + public class ApiResponse where T : class, new() + { + public OperationResult OperationResult { get; set; } + + public T Data { get; set; } + + public ApiResponse() + { + OperationResult = new OperationResult(); + Data = new T(); + } + + public ApiResponse(OperationResult operationResult) + { + OperationResult = operationResult; + Data = new T(); + } + + public ApiResponse(T data) + { + OperationResult = new OperationResult(); + Data = data; + } + + public ApiResponse(OperationResult operationResult, T data) + { + OperationResult = operationResult; + Data = data; + } + } } \ No newline at end of file diff --git a/EstusShots.Shared/Models/Parameters/SeasonParameters.cs b/EstusShots.Shared/Models/Parameters/SeasonParameters.cs new file mode 100644 index 0000000..882b65d --- /dev/null +++ b/EstusShots.Shared/Models/Parameters/SeasonParameters.cs @@ -0,0 +1,124 @@ +using System; +using System.Collections.Generic; +using EstusShots.Shared.Dto; +using EstusShots.Shared.Interfaces; + +namespace EstusShots.Shared.Models.Parameters +{ + // GetSeasons + + /// + /// Parameter class for loading a list of all seasons from the database + /// + public class GetSeasonsParameter : IApiParameter { } + + /// + /// Parameter class returned from the API with all loaded seasons + /// + public class GetSeasonsResponse : IApiResponse + { + /// + /// All existing seasons in the database + /// + public List Seasons { get; set; } + + public GetSeasonsResponse(List seasons) + { + Seasons = seasons; + } + + public GetSeasonsResponse() + { + Seasons = new List(); + } + } + + // GetSeason + + /// + /// Parameter class for loading a single season object + /// + public class GetSeasonParameter : IApiParameter + { + /// + /// ID of the season that should be loaded + /// + public Guid SeasonId { get; set; } + + public GetSeasonParameter() + { + SeasonId = Guid.Empty; + } + + public GetSeasonParameter(Guid seasonId) + { + SeasonId = seasonId; + } + + } + + /// + /// Parameter class returned from the API after loading a single season object + /// + public class GetSeasonResponse : IApiResponse + { + /// + /// The loaded season + /// + public Season Season { get; set; } + + public GetSeasonResponse() + { + Season = new Season(); + } + + public GetSeasonResponse(Season season) + { + Season = season; + } + } + + // SaveSeason + + /// + /// Parameter class for saving season objects + /// + public class SaveSeasonParameter : IApiParameter + { + /// + /// The season object that should be saved + /// + public Season Season { get; set; } + + public SaveSeasonParameter() + { + Season = new Season(); + } + + public SaveSeasonParameter(Season season) + { + Season = season; + } + } + + /// + /// Parameter class returned from the API after saving a season object + /// + public class SaveSeasonResponse : IApiResponse + { + /// + /// ID of the season that was updated or created + /// + public Guid SeasonId { get; set; } + + public SaveSeasonResponse() + { + SeasonId = Guid.Empty; + } + + public SaveSeasonResponse(Guid seasonId) + { + SeasonId = seasonId; + } + } +} \ No newline at end of file diff --git a/EstusShots.sln.DotSettings.user b/EstusShots.sln.DotSettings.user new file mode 100644 index 0000000..d8f3332 --- /dev/null +++ b/EstusShots.sln.DotSettings.user @@ -0,0 +1,3 @@ + + True + True \ No newline at end of file