Skip to content

feature/c-sharp-12-primary-constructors #205

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 4 commits into from
Apr 11, 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
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,6 @@
IValidator<PlayerRequestModel> validator
) : ControllerBase
{
private readonly IPlayerService _playerService = playerService;
private readonly ILogger<PlayerController> _logger = logger;
private readonly IValidator<PlayerRequestModel> validator = validator;

/* -------------------------------------------------------------------------
* HTTP POST
* ---------------------------------------------------------------------- */
Expand All @@ -35,27 +31,32 @@
[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 validation failed: {@Errors}", errors);
logger.LogWarning("POST /players validation failed: {@Errors}", errors);

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

View check run for this annotation

Codeac.io / Codeac Code Quality

CodeDuplication

This block of 10 lines is too similar to src/Dotnet.Samples.AspNetCore.WebApi/Controllers/PlayerController.cs:164
return TypedResults.BadRequest(errors);
}

if (await _playerService.RetrieveByIdAsync(player.Id) != null)
if (await playerService.RetrieveByIdAsync(player.Id) != null)
{
logger.LogWarning(
"POST /players failed: Player with ID {Id} already exists",
player.Id
);
return TypedResults.Conflict();
}

var result = await _playerService.CreateAsync(player);
var result = await playerService.CreateAsync(player);

logger.LogInformation("POST /players created: {@Player}", result);
return TypedResults.CreatedAtRoute(
routeName: "GetById",
routeValues: new { id = result.Id },
Expand All @@ -77,14 +78,16 @@
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IResult> GetAsync()
{
var players = await _playerService.RetrieveAsync();
var players = await playerService.RetrieveAsync();

if (players.Count > 0)
{
logger.LogInformation("GET /players retrieved");
return TypedResults.Ok(players);
}
else
{
logger.LogWarning("GET /players not found");
return TypedResults.NotFound();
}
}
Expand All @@ -100,14 +103,15 @@
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IResult> GetByIdAsync([FromRoute] long id)
{
var player = await _playerService.RetrieveByIdAsync(id);

var player = await playerService.RetrieveByIdAsync(id);
if (player != null)
{
logger.LogInformation("GET /players/{Id} retrieved: {@Player}", id, player);
return TypedResults.Ok(player);
}
else
{
logger.LogWarning("GET /players/{Id} not found", id);
return TypedResults.NotFound();
}
}
Expand All @@ -123,14 +127,19 @@
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IResult> GetBySquadNumberAsync([FromRoute] int squadNumber)
{
var player = await _playerService.RetrieveBySquadNumberAsync(squadNumber);

var player = await playerService.RetrieveBySquadNumberAsync(squadNumber);
if (player != null)
{
logger.LogInformation(
"GET /players/squad/{SquadNumber} retrieved: {@Player}",
squadNumber,
player
);
return TypedResults.Ok(player);
}
else
{
logger.LogWarning("GET /players/squad/{SquadNumber} not found", squadNumber);
return TypedResults.NotFound();
}
}
Expand All @@ -152,27 +161,25 @@
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IResult> PutAsync([FromRoute] long id, [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/{Id} validation failed: {@Errors}", id, errors);
return TypedResults.BadRequest(errors);

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

View check run for this annotation

Codeac.io / Codeac Code Quality

CodeDuplication

This block of 10 lines is too similar to src/Dotnet.Samples.AspNetCore.WebApi/Controllers/PlayerController.cs:34
}

if (await _playerService.RetrieveByIdAsync(id) == null)
if (await playerService.RetrieveByIdAsync(id) == null)
{
logger.LogWarning("PUT /players/{Id} not found", id);
return TypedResults.NotFound();
}

await _playerService.UpdateAsync(player);

await playerService.UpdateAsync(player);
logger.LogInformation("PUT /players/{Id} updated: {@Player}", id, player);
return TypedResults.NoContent();
}

Expand All @@ -191,14 +198,15 @@
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IResult> DeleteAsync([FromRoute] long id)
{
if (await _playerService.RetrieveByIdAsync(id) == null)
if (await playerService.RetrieveByIdAsync(id) == null)
{
logger.LogWarning("DELETE /players/{Id} not found", id);
return TypedResults.NotFound();
}
else
{
await _playerService.DeleteAsync(id);

await playerService.DeleteAsync(id);
logger.LogInformation("DELETE /players/{Id} deleted", id);
return TypedResults.NoContent();
}
}
Expand Down
17 changes: 5 additions & 12 deletions src/Dotnet.Samples.AspNetCore.WebApi/Data/Repository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,15 @@

namespace Dotnet.Samples.AspNetCore.WebApi.Data;

public class Repository<T> : IRepository<T>
public class Repository<T>(DbContext dbContext) : IRepository<T>
where T : class
{
protected readonly DbContext _dbContext;
protected readonly DbSet<T> _dbSet;

public Repository(DbContext dbContext)
{
_dbContext = dbContext;
_dbSet = _dbContext.Set<T>();
}
protected readonly DbSet<T> _dbSet = dbContext.Set<T>();

public async Task AddAsync(T entity)
{
await _dbSet.AddAsync(entity);
await _dbContext.SaveChangesAsync();
await dbContext.SaveChangesAsync();
}

public async Task<List<T>> GetAllAsync() => await _dbSet.AsNoTracking().ToListAsync();
Expand All @@ -27,7 +20,7 @@ public async Task AddAsync(T entity)
public async Task UpdateAsync(T entity)
{
_dbSet.Update(entity);
await _dbContext.SaveChangesAsync();
await dbContext.SaveChangesAsync();
}

public async Task RemoveAsync(long id)
Expand All @@ -36,7 +29,7 @@ public async Task RemoveAsync(long id)
if (entity != null)
{
_dbSet.Remove(entity);
await _dbContext.SaveChangesAsync();
await dbContext.SaveChangesAsync();
}
}
}
109 changes: 55 additions & 54 deletions src/Dotnet.Samples.AspNetCore.WebApi/Services/PlayerService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,30 +12,47 @@ public class PlayerService(
IMapper mapper
) : IPlayerService
{
/// <summary>
/// Creates a MemoryCacheEntryOptions instance with Normal priority,
/// SlidingExpiration of 10 minutes and AbsoluteExpiration of 1 hour.
/// </summary>
private static readonly MemoryCacheEntryOptions CacheEntryOptions =
new MemoryCacheEntryOptions()
.SetPriority(CacheItemPriority.Normal)
.SetSlidingExpiration(TimeSpan.FromMinutes(10))
.SetAbsoluteExpiration(TimeSpan.FromHours(1));

/// <summary>
/// The key used to store the list of Players in the cache.
/// </summary>
private static readonly string CacheKey_RetrieveAsync = nameof(RetrieveAsync);

/// <summary>
/// The key used to store the environment variable for ASP.NET Core.
/// <br/>
/// <see href="https://learn.microsoft.com/en-us/aspnet/core/fundamentals/environments?view=aspnetcore-8.0">
/// Use multiple environments in ASP.NET Core
/// </see>
/// </summary>
private static readonly string AspNetCore_Environment = "ASPNETCORE_ENVIRONMENT";
private static readonly string Development = "Development";

private readonly IPlayerRepository _playerRepository = playerRepository;
private readonly ILogger<PlayerService> _logger = logger;
private readonly IMemoryCache _memoryCache = memoryCache;
private readonly IMapper _mapper = mapper;
/// <summary>
/// The value used to check if the environment is Development.
/// </summary>
private static readonly string Development = "Development";

/* -------------------------------------------------------------------------
* Create
* ---------------------------------------------------------------------- */

public async Task<PlayerResponseModel> CreateAsync(PlayerRequestModel playerRequestModel)
{
var player = _mapper.Map<Player>(playerRequestModel);
await _playerRepository.AddAsync(player);
_logger.LogInformation("Player added to Repository: {Player}", player);
_memoryCache.Remove(CacheKey_RetrieveAsync);
_logger.LogInformation(
"Removed objects from Cache with Key: {Key}",
CacheKey_RetrieveAsync
);
return _mapper.Map<PlayerResponseModel>(player);
var player = mapper.Map<Player>(playerRequestModel);
await playerRepository.AddAsync(player);
logger.LogInformation("Player added to Repository: {Player}", player);
memoryCache.Remove(CacheKey_RetrieveAsync);
logger.LogInformation("Removed objects from Cache with Key: {Key}", CacheKey_RetrieveAsync);
return mapper.Map<PlayerResponseModel>(player);
}

/* -------------------------------------------------------------------------
Expand All @@ -44,48 +61,45 @@ public async Task<PlayerResponseModel> CreateAsync(PlayerRequestModel playerRequ

public async Task<List<PlayerResponseModel>> RetrieveAsync()
{
if (_memoryCache.TryGetValue(CacheKey_RetrieveAsync, out List<PlayerResponseModel>? cached))
if (memoryCache.TryGetValue(CacheKey_RetrieveAsync, out List<PlayerResponseModel>? cached))
{
_logger.LogInformation("Players retrieved from Cache");
logger.LogInformation("Players retrieved from Cache");
return cached!;
}
else
{
// Use multiple environments in ASP.NET Core
// https://learn.microsoft.com/en-us/aspnet/core/fundamentals/environments?view=aspnetcore-8.0
if (Environment.GetEnvironmentVariable(AspNetCore_Environment) == Development)
{
await SimulateRepositoryDelayAsync();
}

var players = await _playerRepository.GetAllAsync();
_logger.LogInformation("Players retrieved from Repository");
var playerResponseModels = _mapper.Map<List<PlayerResponseModel>>(players);
using (var cacheEntry = _memoryCache.CreateEntry(CacheKey_RetrieveAsync))
var players = await playerRepository.GetAllAsync();
logger.LogInformation("Players retrieved from Repository");
var playerResponseModels = mapper.Map<List<PlayerResponseModel>>(players);
using (var cacheEntry = memoryCache.CreateEntry(CacheKey_RetrieveAsync))
{
_logger.LogInformation(
logger.LogInformation(
"{Count} entries created in Cache with key: {Key}",
playerResponseModels.Count,
CacheKey_RetrieveAsync
);
cacheEntry.SetSize(playerResponseModels.Count);
cacheEntry.Value = playerResponseModels;
cacheEntry.SetOptions(GetMemoryCacheEntryOptions());
cacheEntry.SetOptions(CacheEntryOptions);
}
return playerResponseModels;
}
}

public async Task<PlayerResponseModel?> RetrieveByIdAsync(long id)
{
var player = await _playerRepository.FindByIdAsync(id);
return player is not null ? _mapper.Map<PlayerResponseModel>(player) : null;
var player = await playerRepository.FindByIdAsync(id);
return player is not null ? mapper.Map<PlayerResponseModel>(player) : null;
}

public async Task<PlayerResponseModel?> RetrieveBySquadNumberAsync(int squadNumber)
{
var player = await _playerRepository.FindBySquadNumberAsync(squadNumber);
return player is not null ? _mapper.Map<PlayerResponseModel>(player) : null;
var player = await playerRepository.FindBySquadNumberAsync(squadNumber);
return player is not null ? mapper.Map<PlayerResponseModel>(player) : null;
}

/* -------------------------------------------------------------------------
Expand All @@ -94,13 +108,13 @@ public async Task<List<PlayerResponseModel>> RetrieveAsync()

public async Task UpdateAsync(PlayerRequestModel playerRequestModel)
{
if (await _playerRepository.FindByIdAsync(playerRequestModel.Id) is Player player)
if (await playerRepository.FindByIdAsync(playerRequestModel.Id) is Player player)
{
_mapper.Map(playerRequestModel, player);
await _playerRepository.UpdateAsync(player);
_logger.LogInformation("Player updated in Repository: {Player}", player);
_memoryCache.Remove(CacheKey_RetrieveAsync);
_logger.LogInformation(
mapper.Map(playerRequestModel, player);
await playerRepository.UpdateAsync(player);
logger.LogInformation("Player updated in Repository: {Player}", player);
memoryCache.Remove(CacheKey_RetrieveAsync);
logger.LogInformation(
"Removed objects from Cache with Key: {Key}",
CacheKey_RetrieveAsync
);
Expand All @@ -113,31 +127,18 @@ public async Task UpdateAsync(PlayerRequestModel playerRequestModel)

public async Task DeleteAsync(long id)
{
if (await _playerRepository.FindByIdAsync(id) is not null)
if (await playerRepository.FindByIdAsync(id) is not null)
{
await _playerRepository.RemoveAsync(id);
_logger.LogInformation("Player with Id {Id} removed from Repository", id);
_memoryCache.Remove(CacheKey_RetrieveAsync);
_logger.LogInformation(
await playerRepository.RemoveAsync(id);
logger.LogInformation("Player with Id {Id} removed from Repository", id);
memoryCache.Remove(CacheKey_RetrieveAsync);
logger.LogInformation(
"Removed objects from Cache with Key: {Key}",
CacheKey_RetrieveAsync
);
}
}

/// <summary>
/// Creates a MemoryCacheEntryOptions instance with Normal priority,
/// SlidingExpiration of 10 minutes and AbsoluteExpiration of 1 hour.
/// </summary>
/// <returns>A MemoryCacheEntryOptions instance with the specified options.</returns>
private static MemoryCacheEntryOptions GetMemoryCacheEntryOptions()
{
return new MemoryCacheEntryOptions()
.SetPriority(CacheItemPriority.Normal)
.SetSlidingExpiration(TimeSpan.FromMinutes(10))
.SetAbsoluteExpiration(TimeSpan.FromHours(1));
}

/// <summary>
/// Simulates a delay in the repository call to mimic a long-running operation.
/// This is only used in the Development environment to simulate a delay
Expand All @@ -147,7 +148,7 @@ private static MemoryCacheEntryOptions GetMemoryCacheEntryOptions()
private async Task SimulateRepositoryDelayAsync()
{
var milliseconds = new Random().Next(2600, 4200);
_logger.LogInformation(
logger.LogInformation(
"Simulating a random delay of {Milliseconds} milliseconds...",
milliseconds
);
Expand Down
Loading