Skip to content

feature/identifiers #208

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Apr 15, 2025
Merged
Show file tree
Hide file tree
Changes from all 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);
}
21 changes: 21 additions & 0 deletions src/Dotnet.Samples.AspNetCore.WebApi/Data/PlayerDbContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,25 @@ public class PlayerDbContext(DbContextOptions<PlayerDbContext> options) : DbCont
/// <see cref="DbSet{TEntity}"/> corresponds to a table in the database, allowing CRUD operations and LINQ queries.
/// </summary>
public DbSet<Player> Players => Set<Player>();

/// <summary>
/// Configures the model for the Player entity.
/// This method is called by the runtime to configure the model for the context.
/// </summary>
/// <param name="modelBuilder">The model builder.</param>
/// <remarks>
/// This method is used to configure the model and relationships using the Fluent API.
/// It is called when the model for a derived context is being created.
/// </remarks>
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);

modelBuilder.Entity<Player>(entity =>
{
entity.HasKey(player => player.Id);
entity.Property(player => player.Id).ValueGeneratedOnAdd();
entity.HasIndex(player => player.SquadNumber).IsUnique();
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ public sealed class PlayerRepository(PlayerDbContext dbContext)
: Repository<Player>(dbContext),
IPlayerRepository
{
public async ValueTask<Player?> FindBySquadNumberAsync(int squadNumber) =>
public async Task<Player?> FindBySquadNumberAsync(int squadNumber) =>
await _dbSet.FirstOrDefaultAsync(p => p.SquadNumber == squadNumber);

public async Task<bool> SquadNumberExistsAsync(int squadNumber)
{
return await dbContext.Players.AnyAsync(p => p.SquadNumber == squadNumber);
}
}
4 changes: 2 additions & 2 deletions src/Dotnet.Samples.AspNetCore.WebApi/Data/Repository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,15 @@ public async Task AddAsync(T entity)

public async Task<List<T>> GetAllAsync() => await _dbSet.AsNoTracking().ToListAsync();

public async ValueTask<T?> FindByIdAsync(long id) => await _dbSet.FindAsync(id);
public async ValueTask<T?> FindByIdAsync(Guid id) => await _dbSet.FindAsync(id);

public async Task UpdateAsync(T entity)
{
_dbSet.Update(entity);
await dbContext.SaveChangesAsync();
}

public async Task RemoveAsync(long id)
public async Task RemoveAsync(Guid id)
{
var entity = await _dbSet.FindAsync(id);
if (entity != null)
Expand Down
Binary file modified src/Dotnet.Samples.AspNetCore.WebApi/Data/players-sqlite3.db
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
</ItemGroup>

<ItemGroup>
<Content Include="Data/players-sqlite3.db">
<Content Include="Data/players-sqlite3.db" Condition="Exists('Data/players-sqlite3.db')">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<PackageCopyToOutput>true</PackageCopyToOutput>
</Content>
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading