Skip to content

Commit 1d1dcf6

Browse files
committed
refactor!: use GUID internally and expose SquadNumber as public identifier
1 parent 82a9fcf commit 1d1dcf6

29 files changed

+627
-250
lines changed

.codacy.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,4 @@ exclude_paths:
3030
- '**/Utilities/**' # Helper extensions or static classes
3131
- '**/Validators/**' # FluentValidation validators
3232
- 'test/**/*' # Entire test suite (unit + integration)
33+
- 'scripts/**/*' # Helper shell scripts

codecov.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ ignore:
5050
- '**/*.md'
5151

5252
- .*\/test\/.*
53+
- .*\/scripts\/.*
5354
- .*\/Program\.cs
5455
- '**/LICENSE'
5556
- '**/README.md'
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
#!/bin/bash
2+
3+
DATA_FILE="players-sqlite3.db"
4+
5+
PROJECT_ROOT_PATH="src/Dotnet.Samples.AspNetCore.WebApi"
6+
PROJECT_BASE_PATH="$PROJECT_ROOT_PATH/bin/Debug/net8.0"
7+
8+
SOURCE_FILE_PATH="$PROJECT_BASE_PATH/Data/$DATA_FILE"
9+
TARGET_FILE_PATH="$PROJECT_ROOT_PATH/Data/$DATA_FILE"
10+
11+
log() {
12+
local emoji=$1
13+
local level=$2
14+
local message=$3
15+
local timestamp
16+
timestamp=$(date +"%Y-%m-%d %H:%M:%S")
17+
echo "$emoji [$timestamp] [$level] $message"
18+
}
19+
20+
# Check if the EF Core CLI tool is installed
21+
if ! command -v dotnet ef &> /dev/null; then
22+
log "" "ERROR" "'dotnet ef' not found. Install it with 'dotnet tool install --global dotnet-ef'"
23+
exit 1
24+
fi
25+
26+
# Ensure clean placeholder database file exists
27+
log "" "INFO" "Resetting placeholder database at '$TARGET_FILE_PATH'"
28+
rm -f "$TARGET_FILE_PATH"
29+
touch "$TARGET_FILE_PATH"
30+
31+
# Run the database migration
32+
log "" "INFO" "Running EF Core database migration for project at '$PROJECT_ROOT_PATH'..."
33+
dotnet ef database update --project "$PROJECT_ROOT_PATH"
34+
if [ $? -ne 0 ]; then
35+
log "" "ERROR" "Migration failed. See error above."
36+
exit 1
37+
fi
38+
39+
# Check and copy database
40+
if [ -f "$SOURCE_FILE_PATH" ]; then
41+
log "" "INFO" "Found database at '$SOURCE_FILE_PATH'"
42+
log "" "INFO" "Copying to '$TARGET_FILE_PATH'..."
43+
cp -f "$SOURCE_FILE_PATH" "$TARGET_FILE_PATH"
44+
45+
if [ $? -eq 0 ]; then
46+
log "" "INFO" "Database successfully copied to '$TARGET_FILE_PATH'"
47+
else
48+
log "" "ERROR" "Failed to copy the database file."
49+
exit 1
50+
fi
51+
else
52+
log "⚠️" "WARNING" "Database file not found at '$SOURCE_FILE_PATH'."
53+
log "⚠️" "WARNING" "Make sure the migration actually generated the file."
54+
exit 1
55+
fi
56+
57+
# Confirm destination file exists
58+
if [ -f "$TARGET_FILE_PATH" ]; then
59+
log "" "INFO" "Done. The database is now available at '$TARGET_FILE_PATH'"
60+
else
61+
log "⚠️" "WARNING" "Something went wrong. The destination file was not found."
62+
exit 1
63+
fi

src/Dotnet.Samples.AspNetCore.WebApi/Controllers/PlayerController.cs

Lines changed: 34 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using Dotnet.Samples.AspNetCore.WebApi.Models;
33
using Dotnet.Samples.AspNetCore.WebApi.Services;
44
using FluentValidation;
5+
using Microsoft.AspNetCore.Authorization;
56
using Microsoft.AspNetCore.Mvc;
67

78
namespace Dotnet.Samples.AspNetCore.WebApi.Controllers;
@@ -45,11 +46,11 @@ public async Task<IResult> PostAsync([FromBody] PlayerRequestModel player)
4546
return TypedResults.BadRequest(errors);
4647
}
4748

48-
if (await playerService.RetrieveByIdAsync(player.Id) != null)
49+
if (await playerService.RetrieveBySquadNumberAsync(player.SquadNumber) != null)
4950
{
5051
logger.LogWarning(
51-
"POST /players failed: Player with ID {Id} already exists",
52-
player.Id
52+
"POST /players failed: Player with Squad Number {SquadNumber} already exists",
53+
player.SquadNumber
5354
);
5455
return TypedResults.Conflict();
5556
}
@@ -58,8 +59,8 @@ public async Task<IResult> PostAsync([FromBody] PlayerRequestModel player)
5859

5960
logger.LogInformation("POST /players created: {@Player}", result);
6061
return TypedResults.CreatedAtRoute(
61-
routeName: "GetById",
62-
routeValues: new { id = result.Id },
62+
routeName: "GetBySquadNumber",
63+
routeValues: new { squadNumber = result.Dorsal },
6364
value: result
6465
);
6566
}
@@ -98,10 +99,12 @@ public async Task<IResult> GetAsync()
9899
/// <param name="id">The ID of the Player</param>
99100
/// <response code="200">OK</response>
100101
/// <response code="404">Not Found</response>
101-
[HttpGet("{id:long}", Name = "GetById")]
102+
[Authorize(Roles = "Admin")]
103+
[ApiExplorerSettings(IgnoreApi = true)]
104+
[HttpGet("{id:Guid}", Name = "GetById")]
102105
[ProducesResponseType<PlayerResponseModel>(StatusCodes.Status200OK)]
103106
[ProducesResponseType(StatusCodes.Status404NotFound)]
104-
public async Task<IResult> GetByIdAsync([FromRoute] long id)
107+
public async Task<IResult> GetByIdAsync([FromRoute] Guid id)
105108
{
106109
var player = await playerService.RetrieveByIdAsync(id);
107110
if (player != null)
@@ -149,19 +152,23 @@ public async Task<IResult> GetBySquadNumberAsync([FromRoute] int squadNumber)
149152
* ---------------------------------------------------------------------- */
150153

151154
/// <summary>
152-
/// Updates (entirely) a Player by its ID
155+
/// Updates (entirely) a Player by its Squad Number
153156
/// </summary>
154-
/// <param name="id">The ID of the Player</param>
157+
///
155158
/// <param name="player">The PlayerRequestModel</param>
159+
/// <param name="squadNumber">The Squad Number of the Player</param>
156160
/// <response code="204">No Content</response>
157161
/// <response code="400">Bad Request</response>
158162
/// <response code="404">Not Found</response>
159-
[HttpPut("{id}")]
163+
[HttpPut("{squadNumber:int}")]
160164
[Consumes(MediaTypeNames.Application.Json)]
161165
[ProducesResponseType(StatusCodes.Status204NoContent)]
162166
[ProducesResponseType(StatusCodes.Status400BadRequest)]
163167
[ProducesResponseType(StatusCodes.Status404NotFound)]
164-
public async Task<IResult> PutAsync([FromRoute] long id, [FromBody] PlayerRequestModel player)
168+
public async Task<IResult> PutAsync(
169+
[FromRoute] int squadNumber,
170+
[FromBody] PlayerRequestModel player
171+
)
165172
{
166173
var validation = await validator.ValidateAsync(player);
167174
if (!validation.IsValid)
@@ -170,16 +177,20 @@ public async Task<IResult> PutAsync([FromRoute] long id, [FromBody] PlayerReques
170177
.Errors.Select(error => new { error.PropertyName, error.ErrorMessage })
171178
.ToArray();
172179

173-
logger.LogWarning("PUT /players/{Id} validation failed: {@Errors}", id, errors);
180+
logger.LogWarning(
181+
"PUT /players/{squadNumber} validation failed: {@Errors}",
182+
squadNumber,
183+
errors
184+
);
174185
return TypedResults.BadRequest(errors);
175186
}
176-
if (await playerService.RetrieveByIdAsync(id) == null)
187+
if (await playerService.RetrieveBySquadNumberAsync(squadNumber) == null)
177188
{
178-
logger.LogWarning("PUT /players/{Id} not found", id);
189+
logger.LogWarning("PUT /players/{SquadNumber} not found", squadNumber);
179190
return TypedResults.NotFound();
180191
}
181192
await playerService.UpdateAsync(player);
182-
logger.LogInformation("PUT /players/{Id} updated: {@Player}", id, player);
193+
logger.LogInformation("PUT /players/{SquadNumber} updated: {@Player}", squadNumber, player);
183194
return TypedResults.NoContent();
184195
}
185196

@@ -188,25 +199,25 @@ public async Task<IResult> PutAsync([FromRoute] long id, [FromBody] PlayerReques
188199
* ---------------------------------------------------------------------- */
189200

190201
/// <summary>
191-
/// Deletes a Player by its ID
202+
/// Deletes a Player by its Squad Number
192203
/// </summary>
193-
/// <param name="id">The ID of the Player</param>
204+
/// <param name="squadNumber">The Squad Number of the Player</param>
194205
/// <response code="204">No Content</response>
195206
/// <response code="404">Not Found</response>
196-
[HttpDelete("{id:long}")]
207+
[HttpDelete("{squadNumber:int}")]
197208
[ProducesResponseType(StatusCodes.Status204NoContent)]
198209
[ProducesResponseType(StatusCodes.Status404NotFound)]
199-
public async Task<IResult> DeleteAsync([FromRoute] long id)
210+
public async Task<IResult> DeleteAsync([FromRoute] int squadNumber)
200211
{
201-
if (await playerService.RetrieveByIdAsync(id) == null)
212+
if (await playerService.RetrieveBySquadNumberAsync(squadNumber) == null)
202213
{
203-
logger.LogWarning("DELETE /players/{Id} not found", id);
214+
logger.LogWarning("DELETE /players/{SquadNumber} not found", squadNumber);
204215
return TypedResults.NotFound();
205216
}
206217
else
207218
{
208-
await playerService.DeleteAsync(id);
209-
logger.LogInformation("DELETE /players/{Id} deleted", id);
219+
await playerService.DeleteAsync(squadNumber);
220+
logger.LogInformation("DELETE /players/{SquadNumber} deleted", squadNumber);
210221
return TypedResults.NoContent();
211222
}
212223
}

src/Dotnet.Samples.AspNetCore.WebApi/Data/IPlayerRepository.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ public interface IPlayerRepository : IRepository<Player>
1414
/// </summary>
1515
/// <param name="squadNumber">The Squad Number of the Player to retrieve.</param>
1616
/// <returns>
17-
/// A ValueTask representing the asynchronous operation, containing the Player if found,
18-
/// or null if no Player with the specified Squad Number exists.
17+
/// A Task representing the asynchronous operation,containing the Player
18+
/// if found, or null if no Player with the specified Squad Number exists.
1919
/// </returns>
20-
ValueTask<Player?> FindBySquadNumberAsync(int squadNumber);
20+
Task<Player?> FindBySquadNumberAsync(int squadNumber);
2121
}

src/Dotnet.Samples.AspNetCore.WebApi/Data/IRepository.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ public interface IRepository<T>
2929
/// A ValueTask representing the asynchronous operation, containing the entity if found,
3030
/// or null if no entity with the specified ID exists.
3131
/// </returns>
32-
ValueTask<T?> FindByIdAsync(long id);
32+
ValueTask<T?> FindByIdAsync(Guid id);
3333

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

0 commit comments

Comments
 (0)