Skip to content

Commit 7336abe

Browse files
authored
Merge branch 'master' into dependabot/github_actions/codecov/codecov-action-5.4.2
2 parents 3841316 + a06a111 commit 7336abe

29 files changed

+595
-281
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: 37 additions & 26 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;
@@ -26,7 +27,7 @@ IValidator<PlayerRequestModel> validator
2627
/// <response code="201">Created</response>
2728
/// <response code="400">Bad Request</response>
2829
/// <response code="409">Conflict</response>
29-
[HttpPost]
30+
[HttpPost(Name = "Create")]
3031
[Consumes(MediaTypeNames.Application.Json)]
3132
[ProducesResponseType<PlayerResponseModel>(StatusCodes.Status201Created)]
3233
[ProducesResponseType(StatusCodes.Status400BadRequest)]
@@ -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: "RetrieveBySquadNumber",
63+
routeValues: new { squadNumber = result.Dorsal },
6364
value: result
6465
);
6566
}
@@ -73,7 +74,7 @@ public async Task<IResult> PostAsync([FromBody] PlayerRequestModel player)
7374
/// </summary>
7475
/// <response code="200">OK</response>
7576
/// <response code="404">Not Found</response>
76-
[HttpGet]
77+
[HttpGet(Name = "Retrieve")]
7778
[ProducesResponseType<PlayerResponseModel>(StatusCodes.Status200OK)]
7879
[ProducesResponseType(StatusCodes.Status404NotFound)]
7980
public async Task<IResult> GetAsync()
@@ -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 = "RetrieveById")]
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)
@@ -122,7 +125,7 @@ public async Task<IResult> GetByIdAsync([FromRoute] long id)
122125
/// <param name="squadNumber">The Squad Number of the Player</param>
123126
/// <response code="200">OK</response>
124127
/// <response code="404">Not Found</response>
125-
[HttpGet("squad/{squadNumber:int}")]
128+
[HttpGet("squadNumber/{squadNumber:int}", Name = "RetrieveBySquadNumber")]
126129
[ProducesResponseType<PlayerResponseModel>(StatusCodes.Status200OK)]
127130
[ProducesResponseType(StatusCodes.Status404NotFound)]
128131
public async Task<IResult> GetBySquadNumberAsync([FromRoute] int squadNumber)
@@ -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}", Name = "Update")]
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}", Name = "Delete")]
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
}

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

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,25 @@ public class PlayerDbContext(DbContextOptions<PlayerDbContext> options) : DbCont
1414
/// <see cref="DbSet{TEntity}"/> corresponds to a table in the database, allowing CRUD operations and LINQ queries.
1515
/// </summary>
1616
public DbSet<Player> Players => Set<Player>();
17+
18+
/// <summary>
19+
/// Configures the model for the Player entity.
20+
/// This method is called by the runtime to configure the model for the context.
21+
/// </summary>
22+
/// <param name="modelBuilder">The model builder.</param>
23+
/// <remarks>
24+
/// This method is used to configure the model and relationships using the Fluent API.
25+
/// It is called when the model for a derived context is being created.
26+
/// </remarks>
27+
protected override void OnModelCreating(ModelBuilder modelBuilder)
28+
{
29+
base.OnModelCreating(modelBuilder);
30+
31+
modelBuilder.Entity<Player>(entity =>
32+
{
33+
entity.HasKey(player => player.Id);
34+
entity.Property(player => player.Id).ValueGeneratedOnAdd();
35+
entity.HasIndex(player => player.SquadNumber).IsUnique();
36+
});
37+
}
1738
}

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ public sealed class PlayerRepository(PlayerDbContext dbContext)
77
: Repository<Player>(dbContext),
88
IPlayerRepository
99
{
10-
public async ValueTask<Player?> FindBySquadNumberAsync(int squadNumber) =>
10+
public async Task<Player?> FindBySquadNumberAsync(int squadNumber) =>
1111
await _dbSet.FirstOrDefaultAsync(p => p.SquadNumber == squadNumber);
12+
13+
public async Task<bool> SquadNumberExistsAsync(int squadNumber)
14+
{
15+
return await dbContext.Players.AnyAsync(p => p.SquadNumber == squadNumber);
16+
}
1217
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,15 @@ public async Task AddAsync(T entity)
1515

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

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

2020
public async Task UpdateAsync(T entity)
2121
{
2222
_dbSet.Update(entity);
2323
await dbContext.SaveChangesAsync();
2424
}
2525

26-
public async Task RemoveAsync(long id)
26+
public async Task RemoveAsync(Guid id)
2727
{
2828
var entity = await _dbSet.FindAsync(id);
2929
if (entity != null)
16 KB
Binary file not shown.

0 commit comments

Comments
 (0)