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