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 =>
|
||||
builder.Services.AddControllers();
|
||||
builder.Services.Configure<JsonOptions>(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;
|
||||
options.SerializerOptions.WriteIndented = true;
|
||||
});
|
||||
|
||||
JsonConvert.DefaultSettings = () => new JsonSerializerSettings
|
||||
{
|
||||
Formatting = Formatting.Indented,
|
||||
ContractResolver = new DefaultContractResolver
|
||||
{
|
||||
NamingStrategy = new CamelCaseNamingStrategy
|
||||
{
|
||||
ProcessDictionaryKeys = false
|
||||
}
|
||||
},
|
||||
Converters = new[] { new StringEnumConverter() },
|
||||
NullValueHandling = NullValueHandling.Ignore,
|
||||
};
|
||||
}
|
||||
|
||||
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>
|
||||
|
||||
@@ -5,6 +5,5 @@ namespace Shogi.Contracts.Api;
|
||||
public class CreateSessionCommand
|
||||
{
|
||||
[Required]
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public bool IsPrivate { get; set; }
|
||||
public string Name { get; set; }
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ using System.Collections.Generic;
|
||||
|
||||
namespace Shogi.Contracts.Api;
|
||||
|
||||
public class ReadAllSessionsResponse
|
||||
public class ReadSessionsPlayerCountResponse
|
||||
{
|
||||
public IList<SessionMetadata> PlayerHasJoinedSessions { get; set; }
|
||||
public IList<SessionMetadata> AllOtherSessions { get; set; }
|
||||
|
||||
11
Shogi.Contracts/ShogiApiJsonSerializerSettings.cs
Normal file
11
Shogi.Contracts/ShogiApiJsonSerializerSettings.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Shogi.Contracts;
|
||||
|
||||
public class ShogiApiJsonSerializerSettings
|
||||
{
|
||||
public readonly static JsonSerializerOptions SystemTextJsonSerializerOptions = new(JsonSerializerDefaults.Web)
|
||||
{
|
||||
WriteIndented = true,
|
||||
};
|
||||
}
|
||||
@@ -2,9 +2,8 @@
|
||||
|
||||
public class Session
|
||||
{
|
||||
public string Player1 { get; set; } = string.Empty;
|
||||
public string Player1 { get; set; }
|
||||
public string? Player2 { get; set; }
|
||||
public string SessionName { get; set; } = string.Empty;
|
||||
public bool GameOver { get; set; }
|
||||
public string SessionName { get; set; }
|
||||
public BoardState BoardState { get; set; }
|
||||
}
|
||||
|
||||
@@ -11,3 +11,4 @@ Post-Deployment Script Template
|
||||
*/
|
||||
|
||||
:r .\Scripts\PopulateLoginPlatforms.sql
|
||||
:r .\Scripts\PopulatePieces.sql
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER DATABASE Shogi
|
||||
SET ALLOW_SNAPSHOT_ISOLATION ON
|
||||
21
Shogi.Database/Post Deployment/Scripts/PopulatePieces.sql
Normal file
21
Shogi.Database/Post Deployment/Scripts/PopulatePieces.sql
Normal file
@@ -0,0 +1,21 @@
|
||||
DECLARE @Pieces TABLE(
|
||||
[Name] NVARCHAR(13)
|
||||
)
|
||||
|
||||
INSERT INTO @Pieces ([Name])
|
||||
VALUES
|
||||
('King'),
|
||||
('GoldGeneral'),
|
||||
('SilverGeneral'),
|
||||
('Bishop'),
|
||||
('Rook'),
|
||||
('Knight'),
|
||||
('Lance'),
|
||||
('Pawn');
|
||||
|
||||
MERGE [session].[Piece] as t
|
||||
USING @Pieces as s
|
||||
ON t.[Name] = s.[Name]
|
||||
WHEN NOT MATCHED THEN
|
||||
INSERT ([Name])
|
||||
VALUES (s.[Name]);
|
||||
@@ -1,15 +1,12 @@
|
||||
CREATE PROCEDURE [session].[CreateSession]
|
||||
@Name [session].[SessionName],
|
||||
@Player1Name [user].[UserName],
|
||||
@InitialBoardStateDocument [session].[JsonDocument]
|
||||
@Player1Name [user].[UserName]
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
INSERT INTO [session].[Session] (BoardState, Player1Id)
|
||||
SELECT
|
||||
@InitialBoardStateDocument,
|
||||
Id
|
||||
INSERT INTO [session].[Session] ([Name], Player1Id)
|
||||
SELECT @Name, Id
|
||||
FROM [user].[User]
|
||||
WHERE [Name] = @Player1Name
|
||||
END
|
||||
@@ -0,0 +1,5 @@
|
||||
CREATE PROCEDURE [session].[DeleteSession]
|
||||
@Name [session].[SessionName]
|
||||
AS
|
||||
|
||||
DELETE FROM [session].[Session] WHERE [Name] = @Name;
|
||||
@@ -2,15 +2,32 @@
|
||||
@Name [session].[SessionName]
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
SET NOCOUNT ON -- Performance boost
|
||||
SET XACT_ABORT ON -- Rollback transaction on error
|
||||
SET TRANSACTION ISOLATION LEVEL SNAPSHOT -- Ignores data changes that happen after the transaction begins.
|
||||
|
||||
BEGIN TRANSACTION
|
||||
|
||||
-- Session
|
||||
SELECT
|
||||
sess.[Name],
|
||||
BoardState,
|
||||
p1.[Name] as Player1,
|
||||
p2.[Name] as Player2
|
||||
FROM [session].[Session] sess
|
||||
INNER JOIN [user].[User] p1 on sess.Player1Id = p1.Id
|
||||
LEFT JOIN [user].[User] p2 on sess.Player2Id = p2.Id
|
||||
WHERE sess.[Name] = @Name;
|
||||
|
||||
-- Player moves
|
||||
SELECT
|
||||
mv.[From],
|
||||
mv.[To],
|
||||
mv.IsPromotion,
|
||||
piece.[Name] as PieceFromHand
|
||||
FROM [session].[Move] mv
|
||||
INNER JOIN [session].[Session] sess ON sess.Id = mv.SessionId
|
||||
RIGHT JOIN [session].Piece piece on piece.Id = mv.PieceIdFromHand
|
||||
WHERE sess.[Name] = @Name;
|
||||
|
||||
COMMIT
|
||||
END
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
CREATE PROCEDURE [session].[ReadAllSessionsMetadata]
|
||||
CREATE PROCEDURE [session].[ReadSessionPlayerCount]
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
20
Shogi.Database/Session/Tables/Move.sql
Normal file
20
Shogi.Database/Session/Tables/Move.sql
Normal file
@@ -0,0 +1,20 @@
|
||||
CREATE TABLE [session].[Move]
|
||||
(
|
||||
[Id] INT NOT NULL PRIMARY KEY IDENTITY,
|
||||
[SessionId] BIGINT NOT NULL,
|
||||
[From] VARCHAR(2) NOT NULL,
|
||||
[To] VARCHAR(2) NOT NULL,
|
||||
[IsPromotion] BIT NOT NULL,
|
||||
[PieceIdFromHand] INT NULL
|
||||
|
||||
CONSTRAINT [Cannot end where you start]
|
||||
CHECK ([From] <> [To]),
|
||||
|
||||
CONSTRAINT FK_Move_Session FOREIGN KEY (SessionId) REFERENCES [session].[Session] (Id)
|
||||
ON DELETE CASCADE
|
||||
ON UPDATE CASCADE,
|
||||
|
||||
CONSTRAINT FK_Move_Piece FOREIGN KEY (PieceIdFromHand) REFERENCES [session].[Piece] (Id)
|
||||
ON DELETE NO ACTION
|
||||
ON UPDATE NO ACTION
|
||||
)
|
||||
5
Shogi.Database/Session/Tables/Piece.sql
Normal file
5
Shogi.Database/Session/Tables/Piece.sql
Normal file
@@ -0,0 +1,5 @@
|
||||
CREATE TABLE [session].[Piece]
|
||||
(
|
||||
[Id] INT NOT NULL PRIMARY KEY IDENTITY,
|
||||
[Name] NVARCHAR(13) NOT NULL UNIQUE
|
||||
)
|
||||
@@ -4,13 +4,12 @@
|
||||
[Name] [session].[SessionName] UNIQUE,
|
||||
Player1Id BIGINT NOT NULL,
|
||||
Player2Id BIGINT NULL,
|
||||
BoardState [session].[JsonDocument] NOT NULL,
|
||||
Created DATETIMEOFFSET NOT NULL DEFAULT SYSDATETIMEOFFSET(),
|
||||
|
||||
CONSTRAINT [BoardState must be json] CHECK (isjson(BoardState)=1),
|
||||
CONSTRAINT FK_Player1_User FOREIGN KEY (Player1Id) REFERENCES [user].[User] (Id)
|
||||
ON DELETE CASCADE
|
||||
ON UPDATE CASCADE,
|
||||
|
||||
CONSTRAINT FK_Player2_User FOREIGN KEY (Player2Id) REFERENCES [user].[User] (Id)
|
||||
ON DELETE NO ACTION
|
||||
ON UPDATE NO ACTION
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
<SqlServerVerification>False</SqlServerVerification>
|
||||
<IncludeCompositeObjects>True</IncludeCompositeObjects>
|
||||
<TargetDatabaseSet>True</TargetDatabaseSet>
|
||||
<DefaultSchema>session</DefaultSchema>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
||||
<OutputPath>bin\Release\</OutputPath>
|
||||
@@ -77,14 +78,21 @@
|
||||
<Build Include="User\Types\UserName.sql" />
|
||||
<Build Include="Session\Types\JsonDocument.sql" />
|
||||
<Build Include="User\StoredProcedures\CreateUser.sql" />
|
||||
<Build Include="Session\Stored Procedures\ReadAllSessionsMetadata.sql" />
|
||||
<Build Include="Session\Stored Procedures\ReadSessionPlayerCount.sql" />
|
||||
<Build Include="User\StoredProcedures\ReadUser.sql" />
|
||||
<Build Include="User\Tables\LoginPlatform.sql" />
|
||||
<None Include="Post Deployment\Scripts\PopulateLoginPlatforms.sql" />
|
||||
<Build Include="Session\Stored Procedures\UpdateSession.sql" />
|
||||
<Build Include="Session\Stored Procedures\ReadSession.sql" />
|
||||
<Build Include="Session\Tables\Move.sql" />
|
||||
<Build Include="Session\Tables\Piece.sql" />
|
||||
<Build Include="Session\Stored Procedures\DeleteSession.sql" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PostDeploy Include="Post Deployment\Script.PostDeployment.sql" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="Post Deployment\Scripts\PopulatePieces.sql" />
|
||||
<None Include="Post Deployment\Scripts\EnableSnapshotIsolationLevel.sql" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -6,12 +6,11 @@ public class Session
|
||||
{
|
||||
public Session(
|
||||
string name,
|
||||
string player1Name,
|
||||
ShogiBoard board)
|
||||
string player1Name)
|
||||
{
|
||||
Name = name;
|
||||
Player1 = player1Name;
|
||||
Board = board;
|
||||
Board = new(BoardState.StandardStarting);
|
||||
}
|
||||
|
||||
public string Name { get; }
|
||||
|
||||
@@ -9,7 +9,7 @@ public class BoardState
|
||||
/// <summary>
|
||||
/// Board state before any moves have been made, using standard setup and rules.
|
||||
/// </summary>
|
||||
public static readonly BoardState StandardStarting = new(
|
||||
public static BoardState StandardStarting => new(
|
||||
state: BuildStandardStartingBoardState(),
|
||||
player1Hand: new(),
|
||||
player2Hand: new(),
|
||||
|
||||
@@ -8,7 +8,7 @@ namespace Shogi.UI.Pages.Home.Api
|
||||
{
|
||||
Task<CreateGuestTokenResponse?> GetGuestToken();
|
||||
Task<Session?> GetSession(string name);
|
||||
Task<ReadAllSessionsResponse?> GetSessions();
|
||||
Task<ReadSessionsPlayerCountResponse?> GetSessions();
|
||||
Task<Guid?> GetToken();
|
||||
Task GuestLogout();
|
||||
Task PostMove(string sessionName, Move move);
|
||||
|
||||
@@ -63,12 +63,12 @@ namespace Shogi.UI.Pages.Home.Api
|
||||
return null;
|
||||
}
|
||||
|
||||
public async Task<ReadAllSessionsResponse?> GetSessions()
|
||||
public async Task<ReadSessionsPlayerCountResponse?> GetSessions()
|
||||
{
|
||||
var response = await HttpClient.GetAsync(new Uri("Session", UriKind.Relative));
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
return await response.Content.ReadFromJsonAsync<ReadAllSessionsResponse>(serializerOptions);
|
||||
return await response.Content.ReadFromJsonAsync<ReadSessionsPlayerCountResponse>(serializerOptions);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -90,7 +90,6 @@ namespace Shogi.UI.Pages.Home.Api
|
||||
var response = await HttpClient.PostAsJsonAsync(new Uri("Session", UriKind.Relative), new CreateSessionCommand
|
||||
{
|
||||
Name = name,
|
||||
IsPrivate = isPrivate
|
||||
});
|
||||
return response.StatusCode;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Shogi.AcceptanceTests.TestSetup;
|
||||
using Shogi.Contracts.Api;
|
||||
using Shogi.Contracts.Types;
|
||||
using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
using Xunit.Abstractions;
|
||||
@@ -21,16 +22,78 @@ public class AcceptanceTests : IClassFixture<GuestTestFixture>
|
||||
|
||||
private HttpClient Service => fixture.Service;
|
||||
|
||||
[Fact]
|
||||
public async Task ReadSessionsPlayerCount()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Arrange
|
||||
await CreateSession();
|
||||
|
||||
// Act
|
||||
var readAllResponse = await Service
|
||||
.GetFromJsonAsync<ReadSessionsPlayerCountResponse>(new Uri("Sessions/PlayerCount", UriKind.Relative),
|
||||
Contracts.ShogiApiJsonSerializerSettings.SystemTextJsonSerializerOptions);
|
||||
|
||||
// Assert
|
||||
readAllResponse.Should().NotBeNull();
|
||||
readAllResponse!
|
||||
.AllOtherSessions
|
||||
.Should()
|
||||
.ContainSingle(session => session.Name == "Acceptance Tests" && session.PlayerCount == 1);
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Annul
|
||||
await DeleteSession();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateAndReadSession()
|
||||
{
|
||||
var createResponse = await Service.PostAsJsonAsync(new Uri("Session", UriKind.Relative), new CreateSessionCommand { Name = "Acceptance Tests" });
|
||||
createResponse.StatusCode.Should().Be(HttpStatusCode.Created);
|
||||
var yep = await createResponse.Content.ReadAsStringAsync();
|
||||
console.WriteLine(yep);
|
||||
try
|
||||
{
|
||||
// Arrange
|
||||
await CreateSession();
|
||||
|
||||
// Act
|
||||
var response = await Service.GetFromJsonAsync<ReadSessionResponse>(
|
||||
new Uri("Sessions/Acceptance Tests", UriKind.Relative),
|
||||
Contracts.ShogiApiJsonSerializerSettings.SystemTextJsonSerializerOptions);
|
||||
|
||||
// Assert
|
||||
response.Should().NotBeNull();
|
||||
response!.Session.Should().NotBeNull();
|
||||
response.Session.BoardState.Board.Should().NotBeEmpty();
|
||||
response.Session.BoardState.Player1Hand.Should().BeEmpty();
|
||||
response.Session.BoardState.Player2Hand.Should().BeEmpty();
|
||||
response.Session.BoardState.PlayerInCheck.Should().BeNull();
|
||||
response.Session.BoardState.WhoseTurn.Should().Be(WhichPlayer.Player1);
|
||||
response.Session.Player1.Should().NotBeNullOrEmpty();
|
||||
response.Session.Player2.Should().BeNull();
|
||||
response.Session.SessionName.Should().Be("Acceptance Tests");
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Annul
|
||||
await DeleteSession();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CreateSession()
|
||||
{
|
||||
var createResponse = await Service.PostAsJsonAsync(
|
||||
new Uri("Sessions", UriKind.Relative),
|
||||
new CreateSessionCommand { Name = "Acceptance Tests" },
|
||||
Contracts.ShogiApiJsonSerializerSettings.SystemTextJsonSerializerOptions);
|
||||
createResponse.StatusCode.Should().Be(HttpStatusCode.Created);
|
||||
}
|
||||
|
||||
private async Task DeleteSession()
|
||||
{
|
||||
var response = await Service.DeleteAsync(new Uri("Sessions/Acceptance Tests", UriKind.Relative));
|
||||
response.StatusCode.Should().Be(HttpStatusCode.NoContent, because: "Test cleanup should succeed");
|
||||
|
||||
var readAllResponse = await Service.GetFromJsonAsync<ReadAllSessionsResponse>(new Uri("Session", UriKind.Relative));
|
||||
readAllResponse.Should().NotBeNull();
|
||||
readAllResponse!.AllOtherSessions.Should().ContainSingle(session => session.Name == "Acceptance Tests" && session.PlayerCount == 1);
|
||||
}
|
||||
}
|
||||
@@ -21,21 +21,21 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" Version="6.7.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration" Version="6.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="6.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="6.0.1" />
|
||||
<PackageReference Include="Microsoft.Identity.Client" Version="4.46.1" />
|
||||
<PackageReference Include="FluentAssertions" Version="6.8.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration" Version="7.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="7.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="7.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="7.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="7.0.0" />
|
||||
<PackageReference Include="Microsoft.Identity.Client" Version="4.48.0" />
|
||||
<PackageReference Include="Microsoft.Net.Http.Headers" Version="2.2.8" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.0" />
|
||||
<PackageReference Include="xunit" Version="2.4.2" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="coverlet.collector" Version="3.1.2">
|
||||
<PackageReference Include="coverlet.collector" Version="3.2.0">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace Shogi.AcceptanceTests.TestSetup
|
||||
{
|
||||
namespace Shogi.AcceptanceTests.TestSetup;
|
||||
|
||||
/// <summary>
|
||||
/// Acceptance Test fixture for tests which assert features for Microsoft accounts.
|
||||
/// </summary>
|
||||
@@ -13,25 +13,24 @@ namespace Shogi.AcceptanceTests.TestSetup
|
||||
{
|
||||
Configuration = new ConfigurationBuilder()
|
||||
.AddJsonFile("appsettings.json")
|
||||
//.AddEnvironmentVariables()
|
||||
.Build();
|
||||
|
||||
Service = new HttpClient
|
||||
{
|
||||
BaseAddress = new Uri(Configuration["ServiceUrl"], UriKind.Absolute)
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
public IConfiguration Configuration { get; private set; }
|
||||
|
||||
public HttpClient Service { get; }
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
// Log in as a guest account.
|
||||
// Log in as a guest account and retain the session cookie for future requests.
|
||||
var loginResponse = await Service.GetAsync(new Uri("User/LoginAsGuest", UriKind.Relative));
|
||||
loginResponse.EnsureSuccessStatusCode();
|
||||
loginResponse.IsSuccessStatusCode.Should().BeTrue(because: "Guest accounts should work");
|
||||
var guestSessionCookie = loginResponse.Headers.GetValues("Set-Cookie").SingleOrDefault();
|
||||
Service.DefaultRequestHeaders.Add("Set-Cookie", guestSessionCookie);
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
@@ -59,4 +58,3 @@ namespace Shogi.AcceptanceTests.TestSetup
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
namespace Shogi.Domain.UnitTests;
|
||||
|
||||
namespace Shogi.Domain.UnitTests
|
||||
{
|
||||
public class ShogiBoardStateShould
|
||||
{
|
||||
[Fact]
|
||||
public void InitializeBoardState()
|
||||
{
|
||||
// Act
|
||||
var board = new BoardState();
|
||||
var board = BoardState.StandardStarting;
|
||||
|
||||
// Assert
|
||||
board["A1"]?.WhichPiece.Should().Be(WhichPiece.Lance);
|
||||
@@ -183,4 +180,3 @@ namespace Shogi.Domain.UnitTests
|
||||
board["I9"]?.IsPromoted.Should().Be(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using Shogi.Domain.ValueObjects;
|
||||
using System;
|
||||
|
||||
namespace Shogi.Domain.UnitTests
|
||||
{
|
||||
@@ -457,6 +458,6 @@ namespace Shogi.Domain.UnitTests
|
||||
board.InCheck.Should().Be(WhichPlayer.Player2);
|
||||
}
|
||||
|
||||
private static ShogiBoard MockShogiBoard() => new ShogiBoard("Test Session", BoardState.StandardStarting);
|
||||
private static ShogiBoard MockShogiBoard() => new(BoardState.StandardStarting);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,14 +8,14 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" Version="6.7.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.0" />
|
||||
<PackageReference Include="FluentAssertions" Version="6.8.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.0" />
|
||||
<PackageReference Include="xunit" Version="2.4.2" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="coverlet.collector" Version="3.1.2">
|
||||
<PackageReference Include="coverlet.collector" Version="3.2.0">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
|
||||
Reference in New Issue
Block a user