Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .codacy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,4 @@ exclude_paths:
- '**/Utilities/**' # Helper extensions or static classes
- '**/Validators/**' # FluentValidation validators
- 'test/**/*' # Entire test suite (unit + integration)
- 'scripts/**/*' # Helper shell scripts
1 change: 1 addition & 0 deletions codecov.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ ignore:
- '**/*.md'

- .*\/test\/.*
- .*\/scripts\/.*
- .*\/Program\.cs
- '**/LICENSE'
- '**/README.md'
Expand Down
63 changes: 63 additions & 0 deletions scripts/run-migrations-and-copy-database.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
#!/bin/bash

DATA_FILE="players-sqlite3.db"

PROJECT_ROOT_PATH="src/Dotnet.Samples.AspNetCore.WebApi"
PROJECT_BASE_PATH="$PROJECT_ROOT_PATH/bin/Debug/net8.0"

SOURCE_FILE_PATH="$PROJECT_BASE_PATH/Data/$DATA_FILE"
TARGET_FILE_PATH="$PROJECT_ROOT_PATH/Data/$DATA_FILE"

log() {
local emoji=$1
local level=$2
local message=$3
local timestamp
timestamp=$(date +"%Y-%m-%d %H:%M:%S")
echo "$emoji [$timestamp] [$level] $message"
}

# Check if the EF Core CLI tool is installed
if ! command -v dotnet ef &> /dev/null; then
log "❌" "ERROR" "'dotnet ef' not found. Install it with 'dotnet tool install --global dotnet-ef'"
exit 1
fi

# Ensure clean placeholder database file exists
log "✅" "INFO" "Resetting placeholder database at '$TARGET_FILE_PATH'"
rm -f "$TARGET_FILE_PATH"
touch "$TARGET_FILE_PATH"

# Run the database migration
log "✅" "INFO" "Running EF Core database migration for project at '$PROJECT_ROOT_PATH'..."
dotnet ef database update --project "$PROJECT_ROOT_PATH"
if [ $? -ne 0 ]; then

Check notice on line 34 in scripts/run-migrations-and-copy-database.sh

View check run for this annotation

codefactor.io / CodeFactor

scripts/run-migrations-and-copy-database.sh#L34

Check exit code directly with e.g. 'if ! mycmd;', not indirectly with $?. (SC2181)
log "❌" "ERROR" "Migration failed. See error above."
exit 1
fi

# Check and copy database
if [ -f "$SOURCE_FILE_PATH" ]; then
log "✅" "INFO" "Found database at '$SOURCE_FILE_PATH'"
log "✅" "INFO" "Copying to '$TARGET_FILE_PATH'..."
cp -f "$SOURCE_FILE_PATH" "$TARGET_FILE_PATH"

if [ $? -eq 0 ]; then

Check notice on line 45 in scripts/run-migrations-and-copy-database.sh

View check run for this annotation

codefactor.io / CodeFactor

scripts/run-migrations-and-copy-database.sh#L45

Check exit code directly with e.g. 'if mycmd;', not indirectly with $?. (SC2181)
log "✅" "INFO" "Database successfully copied to '$TARGET_FILE_PATH'"
else
log "❌" "ERROR" "Failed to copy the database file."
exit 1
fi
else
log "⚠️" "WARNING" "Database file not found at '$SOURCE_FILE_PATH'."
log "⚠️" "WARNING" "Make sure the migration actually generated the file."
exit 1
fi

# Confirm destination file exists
if [ -f "$TARGET_FILE_PATH" ]; then
log "✅" "INFO" "Done. The database is now available at '$TARGET_FILE_PATH'"
else
log "⚠️" "WARNING" "Something went wrong. The destination file was not found."
exit 1
fi
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using Dotnet.Samples.AspNetCore.WebApi.Models;
using Dotnet.Samples.AspNetCore.WebApi.Services;
using FluentValidation;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

namespace Dotnet.Samples.AspNetCore.WebApi.Controllers;
Expand All @@ -26,30 +27,30 @@
/// <response code="201">Created</response>
/// <response code="400">Bad Request</response>
/// <response code="409">Conflict</response>
[HttpPost]
[HttpPost(Name = "Create")]
[Consumes(MediaTypeNames.Application.Json)]
[ProducesResponseType<PlayerResponseModel>(StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status409Conflict)]
public async Task<IResult> PostAsync([FromBody] PlayerRequestModel player)
{
var validation = await validator.ValidateAsync(player);

if (!validation.IsValid)
{
var errors = validation
.Errors.Select(error => new { error.PropertyName, error.ErrorMessage })
.ToArray();

logger.LogWarning("POST /players validation failed: {@Errors}", errors);
return TypedResults.BadRequest(errors);
}

Check warning on line 47 in src/Dotnet.Samples.AspNetCore.WebApi/Controllers/PlayerController.cs

View check run for this annotation

Codeac.io / Codeac Code Quality

CodeDuplication

This block of 12 lines is too similar to src/Dotnet.Samples.AspNetCore.WebApi/Controllers/PlayerController.cs:170

if (await playerService.RetrieveByIdAsync(player.Id) != null)
if (await playerService.RetrieveBySquadNumberAsync(player.SquadNumber) != null)
{
logger.LogWarning(
"POST /players failed: Player with ID {Id} already exists",
player.Id
"POST /players failed: Player with Squad Number {SquadNumber} already exists",
player.SquadNumber
);
return TypedResults.Conflict();
}
Expand All @@ -58,8 +59,8 @@

logger.LogInformation("POST /players created: {@Player}", result);
return TypedResults.CreatedAtRoute(
routeName: "GetById",
routeValues: new { id = result.Id },
routeName: "RetrieveBySquadNumber",
routeValues: new { squadNumber = result.Dorsal },
value: result
);
}
Expand All @@ -73,7 +74,7 @@
/// </summary>
/// <response code="200">OK</response>
/// <response code="404">Not Found</response>
[HttpGet]
[HttpGet(Name = "Retrieve")]
[ProducesResponseType<PlayerResponseModel>(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IResult> GetAsync()
Expand All @@ -98,10 +99,12 @@
/// <param name="id">The ID of the Player</param>
/// <response code="200">OK</response>
/// <response code="404">Not Found</response>
[HttpGet("{id:long}", Name = "GetById")]
[Authorize(Roles = "Admin")]
[ApiExplorerSettings(IgnoreApi = true)]
[HttpGet("{id:Guid}", Name = "RetrieveById")]
[ProducesResponseType<PlayerResponseModel>(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IResult> GetByIdAsync([FromRoute] long id)
public async Task<IResult> GetByIdAsync([FromRoute] Guid id)
{
var player = await playerService.RetrieveByIdAsync(id);
if (player != null)
Expand All @@ -122,7 +125,7 @@
/// <param name="squadNumber">The Squad Number of the Player</param>
/// <response code="200">OK</response>
/// <response code="404">Not Found</response>
[HttpGet("squad/{squadNumber:int}")]
[HttpGet("squadNumber/{squadNumber:int}", Name = "RetrieveBySquadNumber")]
[ProducesResponseType<PlayerResponseModel>(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IResult> GetBySquadNumberAsync([FromRoute] int squadNumber)
Expand All @@ -149,37 +152,45 @@
* ---------------------------------------------------------------------- */

/// <summary>
/// Updates (entirely) a Player by its ID
/// Updates (entirely) a Player by its Squad Number
/// </summary>
/// <param name="id">The ID of the Player</param>
///
/// <param name="player">The PlayerRequestModel</param>
/// <param name="squadNumber">The Squad Number of the Player</param>
/// <response code="204">No Content</response>
/// <response code="400">Bad Request</response>
/// <response code="404">Not Found</response>
[HttpPut("{id}")]
[HttpPut("{squadNumber:int}", Name = "Update")]
[Consumes(MediaTypeNames.Application.Json)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IResult> PutAsync([FromRoute] long id, [FromBody] PlayerRequestModel player)
public async Task<IResult> PutAsync(
[FromRoute] int squadNumber,
[FromBody] PlayerRequestModel player
)
{
var validation = await validator.ValidateAsync(player);
if (!validation.IsValid)
{
var errors = validation
.Errors.Select(error => new { error.PropertyName, error.ErrorMessage })
.ToArray();

logger.LogWarning("PUT /players/{Id} validation failed: {@Errors}", id, errors);
logger.LogWarning(
"PUT /players/{squadNumber} validation failed: {@Errors}",
squadNumber,

Check warning on line 182 in src/Dotnet.Samples.AspNetCore.WebApi/Controllers/PlayerController.cs

View check run for this annotation

Codeac.io / Codeac Code Quality

CodeDuplication

This block of 12 lines is too similar to src/Dotnet.Samples.AspNetCore.WebApi/Controllers/PlayerController.cs:35
errors
);
return TypedResults.BadRequest(errors);
}
if (await playerService.RetrieveByIdAsync(id) == null)
if (await playerService.RetrieveBySquadNumberAsync(squadNumber) == null)
{
logger.LogWarning("PUT /players/{Id} not found", id);
logger.LogWarning("PUT /players/{SquadNumber} not found", squadNumber);
return TypedResults.NotFound();
}
await playerService.UpdateAsync(player);
logger.LogInformation("PUT /players/{Id} updated: {@Player}", id, player);
logger.LogInformation("PUT /players/{SquadNumber} updated: {@Player}", squadNumber, player);
return TypedResults.NoContent();
}

Expand All @@ -188,25 +199,25 @@
* ---------------------------------------------------------------------- */

/// <summary>
/// Deletes a Player by its ID
/// Deletes a Player by its Squad Number
/// </summary>
/// <param name="id">The ID of the Player</param>
/// <param name="squadNumber">The Squad Number of the Player</param>
/// <response code="204">No Content</response>
/// <response code="404">Not Found</response>
[HttpDelete("{id:long}")]
[HttpDelete("{squadNumber:int}", Name = "Delete")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IResult> DeleteAsync([FromRoute] long id)
public async Task<IResult> DeleteAsync([FromRoute] int squadNumber)
{
if (await playerService.RetrieveByIdAsync(id) == null)
if (await playerService.RetrieveBySquadNumberAsync(squadNumber) == null)
{
logger.LogWarning("DELETE /players/{Id} not found", id);
logger.LogWarning("DELETE /players/{SquadNumber} not found", squadNumber);
return TypedResults.NotFound();
}
else
{
await playerService.DeleteAsync(id);
logger.LogInformation("DELETE /players/{Id} deleted", id);
await playerService.DeleteAsync(squadNumber);
logger.LogInformation("DELETE /players/{SquadNumber} deleted", squadNumber);
return TypedResults.NoContent();
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ public interface IPlayerRepository : IRepository<Player>
/// </summary>
/// <param name="squadNumber">The Squad Number of the Player to retrieve.</param>
/// <returns>
/// A ValueTask representing the asynchronous operation, containing the Player if found,
/// or null if no Player with the specified Squad Number exists.
/// A Task representing the asynchronous operation,containing the Player
/// if found, or null if no Player with the specified Squad Number exists.
/// </returns>
ValueTask<Player?> FindBySquadNumberAsync(int squadNumber);
Task<Player?> FindBySquadNumberAsync(int squadNumber);
}
4 changes: 2 additions & 2 deletions src/Dotnet.Samples.AspNetCore.WebApi/Data/IRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public interface IRepository<T>
/// A ValueTask representing the asynchronous operation, containing the entity if found,
/// or null if no entity with the specified ID exists.
/// </returns>
ValueTask<T?> FindByIdAsync(long id);
ValueTask<T?> FindByIdAsync(Guid id);

/// <summary>
/// Updates an existing entity in the repository.
Expand All @@ -43,5 +43,5 @@ public interface IRepository<T>
/// </summary>
/// <param name="id">The unique identifier of the entity to remove.</param>
/// <returns>A Task representing the asynchronous operation.</returns>
Task RemoveAsync(long id);
Task RemoveAsync(Guid id);
}
Loading
Loading