create, read, playercount
This commit is contained in:
@@ -1,32 +1,32 @@
|
||||
using Shogi.Api.Managers;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Shogi.Api.Extensions;
|
||||
using Shogi.Api.Managers;
|
||||
using Shogi.Api.Repositories;
|
||||
using Shogi.Contracts.Api;
|
||||
using Shogi.Contracts.Socket;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System.Data.SqlClient;
|
||||
using Shogi.Contracts.Types;
|
||||
using Shogi.Api.Extensions;
|
||||
using System.Data.SqlClient;
|
||||
|
||||
namespace Shogi.Api.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("[controller]")]
|
||||
[Authorize]
|
||||
public class SessionController : ControllerBase
|
||||
public class SessionsController : ControllerBase
|
||||
{
|
||||
private readonly ISocketConnectionManager communicationManager;
|
||||
private readonly IModelMapper mapper;
|
||||
private readonly ISessionRepository sessionRepository;
|
||||
private readonly IQueryRespository queryRespository;
|
||||
private readonly ILogger<SessionController> logger;
|
||||
private readonly ILogger<SessionsController> logger;
|
||||
|
||||
public SessionController(
|
||||
public SessionsController(
|
||||
ISocketConnectionManager communicationManager,
|
||||
IModelMapper mapper,
|
||||
ISessionRepository sessionRepository,
|
||||
IQueryRespository queryRespository,
|
||||
ILogger<SessionController> logger)
|
||||
ILogger<SessionsController> logger)
|
||||
{
|
||||
this.communicationManager = communicationManager;
|
||||
this.mapper = mapper;
|
||||
@@ -39,14 +39,10 @@ public class SessionController : ControllerBase
|
||||
public async Task<IActionResult> CreateSession([FromBody] CreateSessionCommand request)
|
||||
{
|
||||
var userId = User.GetShogiUserId();
|
||||
if (string.IsNullOrWhiteSpace(userId)) return this.Unauthorized();
|
||||
var session = new Domain.Aggregates.Session(
|
||||
request.Name,
|
||||
userId,
|
||||
new Domain.ValueObjects.ShogiBoard(Domain.BoardState.StandardStarting));
|
||||
var session = new Domain.Aggregates.Session(request.Name, userId);
|
||||
try
|
||||
{
|
||||
await sessionRepository.CreateShogiBoard(board, request.Name, userId);
|
||||
await sessionRepository.CreateSession(session);
|
||||
}
|
||||
catch (SqlException e)
|
||||
{
|
||||
@@ -58,6 +54,23 @@ public class SessionController : ControllerBase
|
||||
return CreatedAtAction(nameof(CreateSession), new { sessionName = request.Name }, null);
|
||||
}
|
||||
|
||||
[HttpDelete("{name}")]
|
||||
public async Task<IActionResult> DeleteSession(string name)
|
||||
{
|
||||
var userId = User.GetShogiUserId();
|
||||
var session = await sessionRepository.ReadSession(name);
|
||||
|
||||
if (session == null) return this.NoContent();
|
||||
|
||||
if (session.Player1 == userId)
|
||||
{
|
||||
await sessionRepository.DeleteSession(name);
|
||||
return this.NoContent();
|
||||
}
|
||||
|
||||
return this.Unauthorized("Cannot delete sessions created by others.");
|
||||
}
|
||||
|
||||
//[HttpPost("{sessionName}/Move")]
|
||||
//public async Task<IActionResult> MovePiece([FromRoute] string sessionName, [FromBody] MovePieceCommand request)
|
||||
//{
|
||||
@@ -156,12 +169,12 @@ public class SessionController : ControllerBase
|
||||
// return Ok(response);
|
||||
//}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<ReadAllSessionsResponse>> GetSessions()
|
||||
[HttpGet("PlayerCount")]
|
||||
public async Task<ActionResult<ReadSessionsPlayerCountResponse>> GetSessionsPlayerCount()
|
||||
{
|
||||
var sessions = await this.queryRespository.ReadAllSessionsMetadata();
|
||||
var sessions = await this.queryRespository.ReadSessionPlayerCount();
|
||||
|
||||
return Ok(new ReadAllSessionsResponse
|
||||
return Ok(new ReadSessionsPlayerCountResponse
|
||||
{
|
||||
PlayerHasJoinedSessions = Array.Empty<SessionMetadata>(),
|
||||
AllOtherSessions = sessions.ToList()
|
||||
@@ -171,8 +184,26 @@ public class SessionController : ControllerBase
|
||||
[HttpGet("{name}")]
|
||||
public async Task<ActionResult<ReadSessionResponse>> GetSession(string name)
|
||||
{
|
||||
await Task.CompletedTask;
|
||||
return new ReadSessionResponse();
|
||||
var session = await sessionRepository.ReadSession(name);
|
||||
if (session == null) return this.NotFound();
|
||||
|
||||
return new ReadSessionResponse
|
||||
{
|
||||
Session = new Session
|
||||
{
|
||||
BoardState = new BoardState
|
||||
{
|
||||
Board = session.Board.BoardState.State.ToContract(),
|
||||
Player1Hand = session.Board.BoardState.Player1Hand.ToContract(),
|
||||
Player2Hand = session.Board.BoardState.Player2Hand.ToContract(),
|
||||
PlayerInCheck = session.Board.BoardState.InCheck?.ToContract(),
|
||||
WhoseTurn = session.Board.BoardState.WhoseTurn.ToContract()
|
||||
},
|
||||
Player1 = session.Player1,
|
||||
Player2 = session.Player2,
|
||||
SessionName = session.Name
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
//[HttpPut("{sessionName}")]
|
||||
@@ -1,14 +1,11 @@
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Shogi.Contracts.Api;
|
||||
using Shogi.Api.Extensions;
|
||||
using Shogi.Api.Managers;
|
||||
using Shogi.Api.Models;
|
||||
using Shogi.Api.Repositories;
|
||||
using System.Security.Claims;
|
||||
using Shogi.Contracts.Api;
|
||||
|
||||
namespace Shogi.Api.Controllers;
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace Shogi.Api.Extensions;
|
||||
|
||||
public static class Extensions
|
||||
public static class ClaimsExtensions
|
||||
{
|
||||
private static readonly string MsalUsernameClaim = "preferred_username";
|
||||
|
||||
@@ -26,5 +26,17 @@ public static class Extensions
|
||||
return self.Claims.FirstOrDefault(c => c.Type == MsalUsernameClaim)?.Value;
|
||||
}
|
||||
|
||||
public static string? GetShogiUserId(this ClaimsPrincipal self) => self.IsMicrosoft() ? self.GetMicrosoftUserId() : self.GetGuestUserId();
|
||||
}
|
||||
/// <summary>
|
||||
/// Reads the userId from claims after claims transformation has occurred.
|
||||
/// Throws if a shogi userid is not found.
|
||||
/// </summary>
|
||||
/// <exception cref="InvalidOperationException"></exception>
|
||||
public static string GetShogiUserId(this ClaimsPrincipal self)
|
||||
{
|
||||
var id = self.IsMicrosoft() ? self.GetMicrosoftUserId() : self.GetGuestUserId();
|
||||
|
||||
if (string.IsNullOrEmpty(id)) throw new InvalidOperationException("Shogi UserId not found in claims.");
|
||||
|
||||
return id;
|
||||
}
|
||||
}
|
||||
52
Shogi.Api/Extensions/ContractsExtensions.cs
Normal file
52
Shogi.Api/Extensions/ContractsExtensions.cs
Normal file
@@ -0,0 +1,52 @@
|
||||
using Shogi.Contracts.Types;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
|
||||
namespace Shogi.Api.Extensions;
|
||||
|
||||
public static class ContractsExtensions
|
||||
{
|
||||
public static WhichPlayer ToContract(this Domain.WhichPlayer player)
|
||||
{
|
||||
return player switch
|
||||
{
|
||||
Domain.WhichPlayer.Player1 => WhichPlayer.Player1,
|
||||
Domain.WhichPlayer.Player2 => WhichPlayer.Player2,
|
||||
_ => throw new NotImplementedException(),
|
||||
};
|
||||
}
|
||||
|
||||
public static WhichPiece ToContract(this Domain.WhichPiece piece)
|
||||
{
|
||||
return piece switch
|
||||
{
|
||||
Domain.WhichPiece.King => WhichPiece.King,
|
||||
Domain.WhichPiece.GoldGeneral => WhichPiece.GoldGeneral,
|
||||
Domain.WhichPiece.SilverGeneral => WhichPiece.SilverGeneral,
|
||||
Domain.WhichPiece.Bishop => WhichPiece.Bishop,
|
||||
Domain.WhichPiece.Rook => WhichPiece.Rook,
|
||||
Domain.WhichPiece.Knight => WhichPiece.Knight,
|
||||
Domain.WhichPiece.Lance => WhichPiece.Lance,
|
||||
Domain.WhichPiece.Pawn => WhichPiece.Pawn,
|
||||
_ => throw new NotImplementedException(),
|
||||
};
|
||||
}
|
||||
|
||||
public static Piece ToContract(this Domain.ValueObjects.Piece piece) => new()
|
||||
{
|
||||
IsPromoted = piece.IsPromoted,
|
||||
Owner = piece.Owner.ToContract(),
|
||||
WhichPiece = piece.WhichPiece.ToContract()
|
||||
};
|
||||
|
||||
public static IReadOnlyList<Piece> ToContract(this List<Domain.ValueObjects.Piece> pieces)
|
||||
{
|
||||
return pieces
|
||||
.Select(ToContract)
|
||||
.ToList()
|
||||
.AsReadOnly();
|
||||
}
|
||||
|
||||
public static Dictionary<string, Piece?> ToContract(this ReadOnlyDictionary<string, Domain.ValueObjects.Piece?> boardState) =>
|
||||
boardState.ToDictionary(kvp => kvp.Key, kvp => kvp.Value?.ToContract());
|
||||
}
|
||||
@@ -2,12 +2,11 @@ using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.AspNetCore.Http.Extensions;
|
||||
using Microsoft.AspNetCore.Http.Json;
|
||||
using Microsoft.AspNetCore.HttpLogging;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Identity.Web;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
using Newtonsoft.Json.Serialization;
|
||||
using Shogi.Api.Managers;
|
||||
using Shogi.Api.Repositories;
|
||||
using Shogi.Api.Services;
|
||||
@@ -38,7 +37,7 @@ namespace Shogi.Api
|
||||
});
|
||||
|
||||
ConfigureAuthentication(builder);
|
||||
ConfigureControllersWithNewtonsoft(builder);
|
||||
ConfigureControllers(builder);
|
||||
ConfigureSwagger(builder);
|
||||
ConfigureDependencyInjection(builder);
|
||||
ConfigureLogging(builder);
|
||||
@@ -165,39 +164,13 @@ namespace Shogi.Api
|
||||
}
|
||||
}
|
||||
|
||||
private static void ConfigureControllersWithNewtonsoft(WebApplicationBuilder builder)
|
||||
private static void ConfigureControllers(WebApplicationBuilder builder)
|
||||
{
|
||||
builder.Services
|
||||
.AddControllers()
|
||||
//.AddJsonOptions(options =>
|
||||
//{
|
||||
// options.AllowInputFormatterExceptionMessages = true;
|
||||
// options.JsonSerializerOptions.WriteIndented = true;
|
||||
//});
|
||||
.AddNewtonsoftJson(options =>
|
||||
{
|
||||
options.SerializerSettings.Formatting = Formatting.Indented;
|
||||
options.SerializerSettings.ContractResolver = new DefaultContractResolver
|
||||
{
|
||||
NamingStrategy = new CamelCaseNamingStrategy { ProcessDictionaryKeys = false }
|
||||
};
|
||||
options.SerializerSettings.Converters = new[] { new StringEnumConverter() };
|
||||
options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore;
|
||||
});
|
||||
|
||||
JsonConvert.DefaultSettings = () => new JsonSerializerSettings
|
||||
builder.Services.AddControllers();
|
||||
builder.Services.Configure<JsonOptions>(options =>
|
||||
{
|
||||
Formatting = Formatting.Indented,
|
||||
ContractResolver = new DefaultContractResolver
|
||||
{
|
||||
NamingStrategy = new CamelCaseNamingStrategy
|
||||
{
|
||||
ProcessDictionaryKeys = false
|
||||
}
|
||||
},
|
||||
Converters = new[] { new StringEnumConverter() },
|
||||
NullValueHandling = NullValueHandling.Ignore,
|
||||
};
|
||||
options.SerializerOptions.WriteIndented = true;
|
||||
});
|
||||
}
|
||||
|
||||
private static void ConfigureDependencyInjection(WebApplicationBuilder builder)
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
using Shogi.Domain;
|
||||
using Shogi.Domain.ValueObjects;
|
||||
using System.Collections.ObjectModel;
|
||||
|
||||
namespace Shogi.Api.Repositories.Dto;
|
||||
|
||||
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
|
||||
public class BoardStateDto
|
||||
{
|
||||
public ReadOnlyDictionary<string, Piece?> State { get; set; }
|
||||
|
||||
public List<Piece> Player1Hand { get; set; }
|
||||
|
||||
public List<Piece> Player2Hand { get; set; }
|
||||
|
||||
public Move PreviousMove { get; }
|
||||
|
||||
public WhichPlayer WhoseTurn { get; set; }
|
||||
public WhichPlayer? InCheck { get; set; }
|
||||
public bool IsCheckmate { get; set; }
|
||||
}
|
||||
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
|
||||
10
Shogi.Api/Repositories/Dto/MoveDto.cs
Normal file
10
Shogi.Api/Repositories/Dto/MoveDto.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using Shogi.Domain;
|
||||
|
||||
namespace Shogi.Api.Repositories.Dto;
|
||||
|
||||
/// <summary>
|
||||
/// Useful with Dapper to read from database.
|
||||
/// </summary>
|
||||
public readonly record struct MoveDto(string From, string To, bool IsPromotion, WhichPiece? PieceFromHand)
|
||||
{
|
||||
}
|
||||
@@ -1,14 +1,5 @@
|
||||
namespace Shogi.Api.Repositories.Dto;
|
||||
|
||||
/// <summary>
|
||||
/// Useful with Dapper to read from database.
|
||||
/// </summary>
|
||||
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
|
||||
public class SessionDto
|
||||
public readonly record struct SessionDto(string Name, string Player1, string Player2)
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string Player1 { get; set; }
|
||||
public string Player2 { get; set; }
|
||||
public BoardStateDto BoardState { get; set; }
|
||||
}
|
||||
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
|
||||
@@ -1,5 +1,4 @@
|
||||
using Dapper;
|
||||
using Shogi.Api.Repositories.Dto;
|
||||
using Shogi.Contracts.Types;
|
||||
using System.Data.SqlClient;
|
||||
|
||||
@@ -14,16 +13,16 @@ public class QueryRepository : IQueryRespository
|
||||
connectionString = configuration.GetConnectionString("ShogiDatabase");
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<SessionMetadata>> ReadAllSessionsMetadata()
|
||||
public async Task<IEnumerable<SessionMetadata>> ReadSessionPlayerCount()
|
||||
{
|
||||
using var connection = new SqlConnection(connectionString);
|
||||
return await connection.QueryAsync<SessionMetadata>(
|
||||
"session.ReadAllSessionsMetadata",
|
||||
"session.ReadSessionPlayerCount",
|
||||
commandType: System.Data.CommandType.StoredProcedure);
|
||||
}
|
||||
}
|
||||
|
||||
public interface IQueryRespository
|
||||
{
|
||||
Task<IEnumerable<SessionMetadata>> ReadAllSessionsMetadata();
|
||||
Task<IEnumerable<SessionMetadata>> ReadSessionPlayerCount();
|
||||
}
|
||||
@@ -1,11 +1,8 @@
|
||||
using Dapper;
|
||||
using Shogi.Api.Repositories.Dto;
|
||||
using Shogi.Domain;
|
||||
using Shogi.Domain.Aggregates;
|
||||
using Shogi.Domain.ValueObjects;
|
||||
using System.Data;
|
||||
using System.Data.SqlClient;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Shogi.Api.Repositories;
|
||||
|
||||
@@ -20,45 +17,52 @@ public class SessionRepository : ISessionRepository
|
||||
|
||||
public async Task CreateSession(Session session)
|
||||
{
|
||||
var boardStateDto = new BoardStateDto
|
||||
{
|
||||
InCheck = session.BoardState.InCheck,
|
||||
IsCheckmate = session.BoardState.IsCheckmate,
|
||||
Player1Hand = session.BoardState.Player1Hand,
|
||||
Player2Hand = session.BoardState.Player2Hand,
|
||||
State = session.BoardState.State,
|
||||
WhoseTurn = session.BoardState.WhoseTurn,
|
||||
};
|
||||
|
||||
using var connection = new SqlConnection(connectionString);
|
||||
await connection.ExecuteAsync(
|
||||
"session.CreateSession",
|
||||
new
|
||||
{
|
||||
Name = sessionName,
|
||||
InitialBoardStateDocument = JsonSerializer.Serialize(boardStateDto),
|
||||
Player1Name = player1,
|
||||
session.Name,
|
||||
Player1Name = session.Player1,
|
||||
},
|
||||
commandType: CommandType.StoredProcedure);
|
||||
}
|
||||
|
||||
public async Task<ShogiBoard?> ReadShogiBoard(string name)
|
||||
public async Task DeleteSession(string name)
|
||||
{
|
||||
using var connection = new SqlConnection(connectionString);
|
||||
var results = await connection.QueryAsync<SessionDto>(
|
||||
"session.ReadSession",
|
||||
await connection.ExecuteAsync(
|
||||
"session.DeleteSession",
|
||||
new { Name = name },
|
||||
commandType: CommandType.StoredProcedure);
|
||||
var dto = results.SingleOrDefault();
|
||||
if (dto == null) return null;
|
||||
}
|
||||
|
||||
var boardState = new BoardState(
|
||||
state: new(dto.BoardState.State),
|
||||
player1Hand: dto.BoardState.Player1Hand,
|
||||
player2Hand: dto.BoardState.Player2Hand,
|
||||
whoseTurn: dto.BoardState.WhoseTurn,
|
||||
playerInCheck: dto.BoardState.InCheck,
|
||||
previousMove: dto.BoardState.PreviousMove);
|
||||
var session = new ShogiBoard(boardState);
|
||||
public async Task<Session?> ReadSession(string name)
|
||||
{
|
||||
using var connection = new SqlConnection(connectionString);
|
||||
var results = await connection.QueryMultipleAsync(
|
||||
"session.ReadSession",
|
||||
new { Name = name },
|
||||
commandType: CommandType.StoredProcedure);
|
||||
|
||||
var sessionDtos = await results.ReadAsync<SessionDto>();
|
||||
if (!sessionDtos.Any()) return null;
|
||||
var dto = sessionDtos.First();
|
||||
var session = new Session(dto.Name, dto.Player1);
|
||||
if (!string.IsNullOrWhiteSpace(dto.Player2)) session.AddPlayer2(dto.Player2);
|
||||
|
||||
var moveDtos = await results.ReadAsync<MoveDto>();
|
||||
foreach (var move in moveDtos)
|
||||
{
|
||||
if (move.PieceFromHand.HasValue)
|
||||
{
|
||||
session.Board.Move(move.PieceFromHand.Value, move.To);
|
||||
}
|
||||
else
|
||||
{
|
||||
session.Board.Move(move.From, move.To, false);
|
||||
}
|
||||
}
|
||||
return session;
|
||||
}
|
||||
}
|
||||
@@ -66,5 +70,6 @@ public class SessionRepository : ISessionRepository
|
||||
public interface ISessionRepository
|
||||
{
|
||||
Task CreateSession(Session session);
|
||||
Task<ShogiBoard?> ReadShogiBoard(string name);
|
||||
Task DeleteSession(string name);
|
||||
Task<Session?> ReadSession(string name);
|
||||
}
|
||||
@@ -1,12 +1,11 @@
|
||||
using FluentValidation;
|
||||
using Newtonsoft.Json;
|
||||
using Shogi.Contracts.Socket;
|
||||
using Shogi.Contracts.Types;
|
||||
using Shogi.Api.Extensions;
|
||||
using Shogi.Api.Managers;
|
||||
using Shogi.Api.Repositories;
|
||||
using System.Net;
|
||||
using System.Net.WebSockets;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Shogi.Api.Services
|
||||
{
|
||||
@@ -59,7 +58,7 @@ namespace Shogi.Api.Services
|
||||
var message = await socket.ReceiveTextAsync();
|
||||
if (string.IsNullOrWhiteSpace(message)) continue;
|
||||
logger.LogInformation("Request \n{0}\n", message);
|
||||
var request = JsonConvert.DeserializeObject<ISocketRequest>(message);
|
||||
var request = JsonSerializer.Deserialize<ISocketRequest>(message);
|
||||
if (request == null || !Enum.IsDefined(typeof(SocketAction), request.Action))
|
||||
{
|
||||
await socket.SendTextAsync("Error: Action not recognized.");
|
||||
|
||||
@@ -29,12 +29,10 @@
|
||||
<PackageReference Include="FluentValidation" Version="11.2.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.8" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="6.0.8" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="6.0.8" />
|
||||
<PackageReference Include="Microsoft.Identity.Web" Version="1.25.2" />
|
||||
<PackageReference Include="Microsoft.Identity.Web.UI" Version="1.25.2" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
|
||||
<PackageReference Include="System.Data.SqlClient" Version="4.8.4" />
|
||||
<PackageReference Include="System.Data.SqlClient" Version="4.8.5" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
Reference in New Issue
Block a user