diff --git a/Dotnet.Samples.AspNetCore.WebApi.Tests/PlayerServiceTests.cs b/Dotnet.Samples.AspNetCore.WebApi.Tests/PlayerServiceTests.cs deleted file mode 100644 index 3308ad3..0000000 --- a/Dotnet.Samples.AspNetCore.WebApi.Tests/PlayerServiceTests.cs +++ /dev/null @@ -1,246 +0,0 @@ -using System.Data.Common; -using System.Diagnostics; -using Dotnet.Samples.AspNetCore.WebApi.Data; -using Dotnet.Samples.AspNetCore.WebApi.Models; -using Dotnet.Samples.AspNetCore.WebApi.Services; -using FluentAssertions; -using Microsoft.EntityFrameworkCore; -using Moq; - -namespace Dotnet.Samples.AspNetCore.WebApi.Tests; - -public class PlayerServiceTests : IDisposable -{ - private readonly DbConnection _dbConnection; - private readonly DbContextOptions _dbContextOptions; - private readonly PlayerDbContext _dbContext; - - public PlayerServiceTests() - { - (_dbConnection, _dbContextOptions) = PlayerStubs.CreateSqliteConnection(); - _dbContext = PlayerStubs.CreateDbContext(_dbContextOptions); - PlayerStubs.CreateTable(_dbContext); - PlayerStubs.SeedDbContext(_dbContext); - Environment.SetEnvironmentVariable("ASPNETCORE_ENVIRONMENT", "Development"); - } - - public void Dispose() - { - _dbContext.Dispose(); - _dbConnection.Dispose(); - GC.SuppressFinalize(this); - } - - /* ------------------------------------------------------------------------- - * Create - * ---------------------------------------------------------------------- */ - - [Fact] - [Trait("Category", "CreateAsync")] - public async Task GivenCreateAsync_WhenInvokedWithPlayer_ThenShouldAddPlayerToContextAndRemovePlayersCache() - { - // Arrange - var player = PlayerFakes.CreateOneNew(); - var logger = PlayerMocks.LoggerMock(); - var memoryCache = PlayerMocks.MemoryCacheMock(It.IsAny()); - - var service = new PlayerService(_dbContext, logger.Object, memoryCache.Object); - - // Act - await service.CreateAsync(player); - var result = await _dbContext.Players.FindAsync(player.Id); - - // Assert - result.Should().NotBeNull(); - memoryCache.Verify(cache => cache.Remove(It.IsAny()), Times.Exactly(1)); - } - - /* ------------------------------------------------------------------------- - * Retrieve - * ---------------------------------------------------------------------- */ - - [Fact] - [Trait("Category", "RetrieveAsync")] - public async Task GivenRetrieveAsync_WhenInvoked_ThenShouldReturnAllPlayersAndCreatePlayersCache() - { - // Arrange - var players = PlayerFakes.CreateStarting11(); - var logger = PlayerMocks.LoggerMock(); - var memoryCache = PlayerMocks.MemoryCacheMock(It.IsAny()); - var value = It.IsAny(); - - var service = new PlayerService(_dbContext, logger.Object, memoryCache.Object); - - // Act - var result = await service.RetrieveAsync(); - - // Assert - memoryCache.Verify( - cache => cache.TryGetValue(It.IsAny(), out value), - Times.Exactly(1) - ); - memoryCache.Verify(cache => cache.CreateEntry(It.IsAny()), Times.Exactly(1)); - result.Should().BeEquivalentTo(players); - } - - [Fact] - [Trait("Category", "RetrieveAsync")] - public async Task GivenRetrieveAsync_WhenInvokedTwice_ThenSecondExecutionTimeShouldBeLessThanFirst() - { - // Arrange - var players = PlayerFakes.CreateStarting11(); - var logger = PlayerMocks.LoggerMock(); - var memoryCache = PlayerMocks.MemoryCacheMock(players); - var value = It.IsAny(); - - var service = new PlayerService(_dbContext, logger.Object, memoryCache.Object); - - // Act - var first = await ExecutionTimeAsync(() => service.RetrieveAsync()); - var second = await ExecutionTimeAsync(() => service.RetrieveAsync()); - - // Assert - memoryCache.Verify( - cache => cache.TryGetValue(It.IsAny(), out value), - Times.Exactly(2) // first + second - ); - second.Should().BeLessThan(first); - } - - [Fact] - [Trait("Category", "RetrieveByIdAsync")] - public async Task GivenRetrieveByIdAsync_WhenInvokedWithNonexistentId_ThenShouldReturnNull() - { - // Arrange - var id = 999; - var logger = PlayerMocks.LoggerMock(); - var memoryCache = PlayerMocks.MemoryCacheMock(It.IsAny()); - - var service = new PlayerService(_dbContext, logger.Object, memoryCache.Object); - - // Act - var result = await service.RetrieveByIdAsync(id); - - // Assert - result.Should().BeNull(); - } - - [Fact] - [Trait("Category", "RetrieveByIdAsync")] - public async Task GivenRetrieveByIdAsync_WhenInvokedWithExistingId_ThenShouldReturnPlayer() - { - // Arrange - var player = PlayerFakes.CreateOneByIdFromStarting11(10); - var logger = PlayerMocks.LoggerMock(); - var memoryCache = PlayerMocks.MemoryCacheMock(It.IsAny()); - - var service = new PlayerService(_dbContext, logger.Object, memoryCache.Object); - - // Act - var result = await service.RetrieveByIdAsync(player.Id); - - // Assert - result.Should().BeOfType(); - result.Should().BeEquivalentTo(player); - } - - [Fact] - [Trait("Category", "RetrieveBySquadNumberAsync")] - public async Task GivenRetrieveBySquadNumberAsync_WhenInvokedWithNonexistentSquadNumber_ThenShouldReturnNull() - { - // Arrange - var squadNumber = 999; - var logger = PlayerMocks.LoggerMock(); - var memoryCache = PlayerMocks.MemoryCacheMock(It.IsAny()); - - var service = new PlayerService(_dbContext, logger.Object, memoryCache.Object); - - // Act - var result = await service.RetrieveBySquadNumberAsync(squadNumber); - - // Assert - result.Should().BeNull(); - } - - [Fact] - [Trait("Category", "RetrieveBySquadNumberAsync")] - public async Task GivenRetrieveBySquadNumberAsync_WhenInvokedWithExistingSquadNumber_ThenShouldReturnPlayer() - { - // Arrange - var player = PlayerFakes.CreateOneByIdFromStarting11(6); - var logger = PlayerMocks.LoggerMock(); - var memoryCache = PlayerMocks.MemoryCacheMock(It.IsAny()); - - var service = new PlayerService(_dbContext, logger.Object, memoryCache.Object); - - // Act - var result = await service.RetrieveBySquadNumberAsync(player.SquadNumber); - - // Assert - result.Should().BeOfType(); - result.Should().BeEquivalentTo(player); - } - - /* ------------------------------------------------------------------------- - * Update - * ---------------------------------------------------------------------- */ - - [Fact] - [Trait("Category", "UpdateAsync")] - public async Task GivenUpdateAsync_WhenInvokedWithPlayer_ThenShouldModifyPlayerInContextAndRemovePlayersCache() - { - // Arrange - var player = PlayerFakes.CreateOneByIdFromStarting11(1); - var logger = PlayerMocks.LoggerMock(); - var memoryCache = PlayerMocks.MemoryCacheMock(It.IsAny()); - - var service = new PlayerService(_dbContext, logger.Object, memoryCache.Object); - - // Act - player.FirstName = "Emiliano"; - player.MiddleName = ""; - await service.UpdateAsync(player); - var result = await _dbContext.Players.FindAsync(player.Id); - - // Assert - result!.FirstName.Should().Be(player.FirstName); - memoryCache.Verify(cache => cache.Remove(It.IsAny()), Times.Exactly(1)); - } - - /* ------------------------------------------------------------------------- - * Delete - * ---------------------------------------------------------------------- */ - - [Fact] - [Trait("Category", "DeleteAsync")] - public async Task GivenDeleteAsync_WhenInvokedWithId_ThenShouldRemovePlayerFromContextAndRemovePlayersCache() - { - // Arrange - var player = PlayerFakes.CreateOneNew(); - var logger = PlayerMocks.LoggerMock(); - var memoryCache = PlayerMocks.MemoryCacheMock(It.IsAny()); - await _dbContext.AddAsync(player); - await _dbContext.SaveChangesAsync(); - - var service = new PlayerService(_dbContext, logger.Object, memoryCache.Object); - - // Act - await service.DeleteAsync(player.Id); - var result = await _dbContext.Players.FindAsync(player.Id); - - // Assert - result.Should().BeNull(); - memoryCache.Verify(cache => cache.Remove(It.IsAny()), Times.Exactly(1)); - } - - private async Task ExecutionTimeAsync(Func awaitable) - { - var stopwatch = new Stopwatch(); - - stopwatch.Start(); - await awaitable(); - stopwatch.Stop(); - - return stopwatch.ElapsedMilliseconds; - } -} diff --git a/Dotnet.Samples.AspNetCore.WebApi.Tests/PlayerControllerTests.cs b/Dotnet.Samples.AspNetCore.WebApi.Tests/Unit/PlayerControllerTests.cs similarity index 77% rename from Dotnet.Samples.AspNetCore.WebApi.Tests/PlayerControllerTests.cs rename to Dotnet.Samples.AspNetCore.WebApi.Tests/Unit/PlayerControllerTests.cs index 4ea843c..84bbfeb 100644 --- a/Dotnet.Samples.AspNetCore.WebApi.Tests/PlayerControllerTests.cs +++ b/Dotnet.Samples.AspNetCore.WebApi.Tests/Unit/PlayerControllerTests.cs @@ -1,28 +1,35 @@ using Dotnet.Samples.AspNetCore.WebApi.Controllers; using Dotnet.Samples.AspNetCore.WebApi.Models; using Dotnet.Samples.AspNetCore.WebApi.Services; +using Dotnet.Samples.AspNetCore.WebApi.Tests.Utilities; using FluentAssertions; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.HttpResults; using Moq; -namespace Dotnet.Samples.AspNetCore.WebApi.Tests; +namespace Dotnet.Samples.AspNetCore.WebApi.Tests.Unit; -public class PlayerControllerTests +public class PlayerControllerTests : IDisposable { + private bool _disposed; + + public PlayerControllerTests() + { + Environment.SetEnvironmentVariable("ASPNETCORE_ENVIRONMENT", "Development"); + } + /* ------------------------------------------------------------------------- * HTTP POST * ---------------------------------------------------------------------- */ [Fact] - [Trait("Category", "PostAsync")] + [Trait("Category", "Unit")] public async Task GivenPostAsync_WhenModelStateIsInvalid_ThenResponseStatusCodeShouldBe400BadRequest() { // Arrange - var service = new Mock(); - var logger = PlayerMocks.LoggerMock(); + var (service, logger) = PlayerMocks.SetupControllerMocks(); - var controller = new PlayersController(service.Object, logger.Object); + var controller = new PlayerController(service.Object, logger.Object); controller.ModelState.Merge(PlayerStubs.CreateModelError("FirstName", "Required")); // Act @@ -34,16 +41,15 @@ public async Task GivenPostAsync_WhenModelStateIsInvalid_ThenResponseStatusCodeS } [Fact] - [Trait("Category", "PostAsync")] + [Trait("Category", "Unit")] public async Task GivenPostAsync_WhenServiceRetrieveByIdAsyncReturnsPlayer_ThenResponseStatusCodeShouldBe409Conflict() { // Arrange var player = PlayerFakes.CreateOneByIdFromStarting11(10); - var service = new Mock(); + var (service, logger) = PlayerMocks.SetupControllerMocks(); service.Setup(service => service.RetrieveByIdAsync(It.IsAny())).ReturnsAsync(player); - var logger = PlayerMocks.LoggerMock(); - var controller = new PlayersController(service.Object, logger.Object); + var controller = new PlayerController(service.Object, logger.Object); // Act var response = await controller.PostAsync(player) as Conflict; @@ -55,20 +61,18 @@ public async Task GivenPostAsync_WhenServiceRetrieveByIdAsyncReturnsPlayer_ThenR } [Fact] - [Trait("Category", "PostAsync")] + [Trait("Category", "Unit")] public async Task GivenPostAsync_WhenServiceRetrieveByIdAsyncReturnsNull_ThenResponseStatusCodeShouldBe201Created() { // Arrange var player = PlayerFakes.CreateOneNew(); - - var service = new Mock(); + var (service, logger) = PlayerMocks.SetupControllerMocks(); service .Setup(service => service.RetrieveByIdAsync(It.IsAny())) .ReturnsAsync(null as Player); service.Setup(service => service.CreateAsync(It.IsAny())); - var logger = PlayerMocks.LoggerMock(); - var controller = new PlayersController(service.Object, logger.Object) + var controller = new PlayerController(service.Object, logger.Object) { Url = PlayerMocks.UrlHelperMock().Object, }; @@ -88,16 +92,15 @@ public async Task GivenPostAsync_WhenServiceRetrieveByIdAsyncReturnsNull_ThenRes * ---------------------------------------------------------------------- */ [Fact] - [Trait("Category", "GetAsync")] + [Trait("Category", "Unit")] public async Task GivenGetAsync_WhenServiceRetrieveAsyncReturnsListOfPlayers_ThenResponseShouldBeEquivalentToListOfPlayers() { // Arrange var players = PlayerFakes.CreateStarting11(); - var service = new Mock(); + var (service, logger) = PlayerMocks.SetupControllerMocks(); service.Setup(service => service.RetrieveAsync()).ReturnsAsync(players); - var logger = PlayerMocks.LoggerMock(); - var controller = new PlayersController(service.Object, logger.Object); + var controller = new PlayerController(service.Object, logger.Object); // Act var response = await controller.GetAsync() as Ok>; @@ -111,16 +114,15 @@ public async Task GivenGetAsync_WhenServiceRetrieveAsyncReturnsListOfPlayers_The } [Fact] - [Trait("Category", "GetAsync")] + [Trait("Category", "Unit")] public async Task GivenGetAsync_WhenServiceRetrieveAsyncReturnsEmptyList_ThenResponseStatusCodeShouldBe404NotFound() { // Arrange var players = new List(); // Count = 0 - var service = new Mock(); + var (service, logger) = PlayerMocks.SetupControllerMocks(); service.Setup(service => service.RetrieveAsync()).ReturnsAsync(players); - var logger = PlayerMocks.LoggerMock(); - var controller = new PlayersController(service.Object, logger.Object); + var controller = new PlayerController(service.Object, logger.Object); // Act var response = await controller.GetAsync() as NotFound; @@ -132,17 +134,16 @@ public async Task GivenGetAsync_WhenServiceRetrieveAsyncReturnsEmptyList_ThenRes } [Fact] - [Trait("Category", "GetByIdAsync")] + [Trait("Category", "Unit")] public async Task GivenGetByIdAsync_WhenServiceRetrieveByIdAsyncReturnsNull_ThenResponseStatusCodeShouldBe404NotFound() { // Arrange - var service = new Mock(); + var (service, logger) = PlayerMocks.SetupControllerMocks(); service .Setup(service => service.RetrieveByIdAsync(It.IsAny())) .ReturnsAsync(null as Player); - var logger = PlayerMocks.LoggerMock(); - var controller = new PlayersController(service.Object, logger.Object); + var controller = new PlayerController(service.Object, logger.Object); // Act var response = await controller.GetByIdAsync(It.IsAny()) as NotFound; @@ -154,16 +155,15 @@ public async Task GivenGetByIdAsync_WhenServiceRetrieveByIdAsyncReturnsNull_Then } [Fact] - [Trait("Category", "GetByIdAsync")] + [Trait("Category", "Unit")] public async Task GivenGetByIdAsync_WhenServiceRetrieveByIdAsyncReturnsPlayer_ThenResponseStatusCodeShouldBe200Ok() { // Arrange var player = PlayerFakes.CreateOneByIdFromStarting11(10); - var service = new Mock(); + var (service, logger) = PlayerMocks.SetupControllerMocks(); service.Setup(service => service.RetrieveByIdAsync(It.IsAny())).ReturnsAsync(player); - var logger = PlayerMocks.LoggerMock(); - var controller = new PlayersController(service.Object, logger.Object); + var controller = new PlayerController(service.Object, logger.Object); // Act var response = await controller.GetByIdAsync(It.IsAny()) as Ok; @@ -177,17 +177,16 @@ public async Task GivenGetByIdAsync_WhenServiceRetrieveByIdAsyncReturnsPlayer_Th } [Fact] - [Trait("Category", "GetBySquadNumberAsync")] + [Trait("Category", "Unit")] public async Task GivenGetBySquadNumberAsync_WhenServiceRetrieveBySquadNumberAsyncReturnsNull_ThenResponseStatusCodeShouldBe404NotFound() { // Arrange - var service = new Mock(); + var (service, logger) = PlayerMocks.SetupControllerMocks(); service .Setup(service => service.RetrieveBySquadNumberAsync(It.IsAny())) .ReturnsAsync(null as Player); - var logger = PlayerMocks.LoggerMock(); - var controller = new PlayersController(service.Object, logger.Object); + var controller = new PlayerController(service.Object, logger.Object); // Act var response = await controller.GetBySquadNumberAsync(It.IsAny()) as NotFound; @@ -202,18 +201,17 @@ public async Task GivenGetBySquadNumberAsync_WhenServiceRetrieveBySquadNumberAsy } [Fact] - [Trait("Category", "GetBySquadNumberAsync")] + [Trait("Category", "Unit")] public async Task GivenGetBySquadNumberAsync_WhenServiceRetrieveBySquadNumberAsyncReturnsPlayer_ThenResponseStatusCodeShouldBe200Ok() { // Arrange var player = PlayerFakes.CreateOneByIdFromStarting11(10); - var service = new Mock(); + var (service, logger) = PlayerMocks.SetupControllerMocks(); service .Setup(service => service.RetrieveBySquadNumberAsync(It.IsAny())) .ReturnsAsync(player); - var logger = PlayerMocks.LoggerMock(); - var controller = new PlayersController(service.Object, logger.Object); + var controller = new PlayerController(service.Object, logger.Object); // Act var response = await controller.GetBySquadNumberAsync(It.IsAny()) as Ok; @@ -234,14 +232,13 @@ public async Task GivenGetBySquadNumberAsync_WhenServiceRetrieveBySquadNumberAsy * ---------------------------------------------------------------------- */ [Fact] - [Trait("Category", "PutAsync")] + [Trait("Category", "Unit")] public async Task GivenPutAsync_WhenModelStateIsInvalid_ThenResponseStatusCodeShouldBe400BadRequest() { // Arrange - var service = new Mock(); - var logger = PlayerMocks.LoggerMock(); + var (service, logger) = PlayerMocks.SetupControllerMocks(); - var controller = new PlayersController(service.Object, logger.Object); + var controller = new PlayerController(service.Object, logger.Object); controller.ModelState.Merge(PlayerStubs.CreateModelError("FirstName", "Required")); // Act @@ -254,7 +251,7 @@ public async Task GivenPutAsync_WhenModelStateIsInvalid_ThenResponseStatusCodeSh } [Fact] - [Trait("Category", "PutAsync")] + [Trait("Category", "Unit")] public async Task GivenPutAsync_WhenServiceRetrieveByIdAsyncReturnsNull_ThenResponseStatusCodeShouldBe404NotFound() { // Arrange @@ -262,9 +259,9 @@ public async Task GivenPutAsync_WhenServiceRetrieveByIdAsyncReturnsNull_ThenResp service .Setup(service => service.RetrieveByIdAsync(It.IsAny())) .ReturnsAsync(null as Player); - var logger = PlayerMocks.LoggerMock(); + var logger = PlayerMocks.LoggerMock(); - var controller = new PlayersController(service.Object, logger.Object); + var controller = new PlayerController(service.Object, logger.Object); // Act var response = await controller.PutAsync(It.IsAny(), It.IsAny()) as NotFound; @@ -276,18 +273,17 @@ public async Task GivenPutAsync_WhenServiceRetrieveByIdAsyncReturnsNull_ThenResp } [Fact] - [Trait("Category", "PutAsync")] + [Trait("Category", "Unit")] public async Task GivenPutAsync_WhenServiceRetrieveByIdAsyncReturnsPlayer_ThenResponseStatusCodeShouldBe204NoContent() { // Arrange var id = 10; var player = PlayerFakes.CreateOneByIdFromStarting11(id); - var service = new Mock(); + var (service, logger) = PlayerMocks.SetupControllerMocks(); service.Setup(service => service.RetrieveByIdAsync(It.IsAny())).ReturnsAsync(player); service.Setup(service => service.UpdateAsync(It.IsAny())); - var logger = PlayerMocks.LoggerMock(); - var controller = new PlayersController(service.Object, logger.Object); + var controller = new PlayerController(service.Object, logger.Object); // Act var response = await controller.PutAsync(id, player) as NoContent; @@ -304,17 +300,16 @@ public async Task GivenPutAsync_WhenServiceRetrieveByIdAsyncReturnsPlayer_ThenRe * ---------------------------------------------------------------------- */ [Fact] - [Trait("Category", "DeleteAsync")] + [Trait("Category", "Unit")] public async Task GivenDeleteAsync_WhenServiceRetrieveByIdAsyncReturnsNull_ThenResponseStatusCodeShouldBe404NotFound() { // Arrange - var service = new Mock(); + var (service, logger) = PlayerMocks.SetupControllerMocks(); service .Setup(service => service.RetrieveByIdAsync(It.IsAny())) .ReturnsAsync(null as Player); - var logger = PlayerMocks.LoggerMock(); - var controller = new PlayersController(service.Object, logger.Object); + var controller = new PlayerController(service.Object, logger.Object); // Act var response = await controller.DeleteAsync(It.IsAny()) as NotFound; @@ -326,17 +321,16 @@ public async Task GivenDeleteAsync_WhenServiceRetrieveByIdAsyncReturnsNull_ThenR } [Fact] - [Trait("Category", "DeleteAsync")] + [Trait("Category", "Unit")] public async Task GivenDeleteAsync_WhenServiceRetrieveByIdAsyncReturnsPlayer_ThenResponseStatusCodeShouldBe204NoContent() { // Arrange var player = PlayerFakes.CreateOneByIdFromStarting11(10); - var service = new Mock(); + var (service, logger) = PlayerMocks.SetupControllerMocks(); service.Setup(service => service.RetrieveByIdAsync(It.IsAny())).ReturnsAsync(player); service.Setup(service => service.DeleteAsync(It.IsAny())); - var logger = PlayerMocks.LoggerMock(); - var controller = new PlayersController(service.Object, logger.Object); + var controller = new PlayerController(service.Object, logger.Object); // Act var response = await controller.DeleteAsync(It.IsAny()) as NoContent; @@ -347,4 +341,19 @@ public async Task GivenDeleteAsync_WhenServiceRetrieveByIdAsyncReturnsPlayer_The response.Should().NotBeNull().And.BeOfType(); response?.StatusCode.Should().Be(StatusCodes.Status204NoContent); } + + protected virtual void Dispose(bool disposing) + { + if (!_disposed) + { + Environment.SetEnvironmentVariable("ASPNETCORE_ENVIRONMENT", null); + _disposed = true; + } + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } } diff --git a/Dotnet.Samples.AspNetCore.WebApi.Tests/Unit/PlayerServiceTests.cs b/Dotnet.Samples.AspNetCore.WebApi.Tests/Unit/PlayerServiceTests.cs new file mode 100644 index 0000000..53a262d --- /dev/null +++ b/Dotnet.Samples.AspNetCore.WebApi.Tests/Unit/PlayerServiceTests.cs @@ -0,0 +1,250 @@ +using System.Diagnostics; +using Dotnet.Samples.AspNetCore.WebApi.Models; +using Dotnet.Samples.AspNetCore.WebApi.Services; +using Dotnet.Samples.AspNetCore.WebApi.Tests.Utilities; +using FluentAssertions; +using Moq; + +namespace Dotnet.Samples.AspNetCore.WebApi.Tests.Unit; + +public class PlayerServiceTests : IDisposable +{ + private bool _disposed; + + public PlayerServiceTests() + { + Environment.SetEnvironmentVariable("ASPNETCORE_ENVIRONMENT", "Development"); + } + + /* ------------------------------------------------------------------------- + * Create + * ---------------------------------------------------------------------- */ + + [Fact] + [Trait("Category", "Unit")] + public async Task GivenCreateAsync_WhenRepositoryAddAsync_ThenAddsPlayerToRepositoryAndRemovesMemoryCache() + { + // Arrange + var (repository, logger, memoryCache) = PlayerMocks.SetupServiceMocks(); + + var service = new PlayerService(repository.Object, logger.Object, memoryCache.Object); + + // Act + await service.CreateAsync(It.IsAny()); + + // Assert + repository.Verify(repository => repository.AddAsync(It.IsAny()), Times.Once); + memoryCache.Verify(cache => cache.Remove(It.IsAny()), Times.Once); + } + + /* ------------------------------------------------------------------------- + * Retrieve + * ---------------------------------------------------------------------- */ + + [Fact] + [Trait("Category", "Unit")] + public async Task GivenRetrieveAsync_WhenRepositoryGetAllAsyncReturnsPlayers_ThenCacheCreateEntryAndResultShouldBeListOfPlayers() + { + // Arrange + var players = PlayerFakes.CreateStarting11(); + var (repository, logger, memoryCache) = PlayerMocks.SetupServiceMocks(); + repository.Setup(repository => repository.GetAllAsync()).ReturnsAsync(players); + var value = It.IsAny(); + + var service = new PlayerService(repository.Object, logger.Object, memoryCache.Object); + + // Act + var result = await service.RetrieveAsync(); + + // Assert + repository.Verify(repository => repository.GetAllAsync(), Times.Once); + memoryCache.Verify(cache => cache.TryGetValue(It.IsAny(), out value), Times.Once); + memoryCache.Verify(cache => cache.CreateEntry(It.IsAny()), Times.Once); + result.Should().BeEquivalentTo(players); + } + + [Fact] + [Trait("Category", "Unit")] + public async Task GivenRetrieveAsync_WhenExecutedForTheSecondTime_ThenSecondExecutionTimeShouldBeLessThanFirst() + { + // Arrange + var players = PlayerFakes.CreateStarting11(); + var (repository, logger, memoryCache) = PlayerMocks.SetupServiceMocks(cacheValue: players); + var value = It.IsAny(); + + var service = new PlayerService(repository.Object, logger.Object, memoryCache.Object); + + // Act + var first = await ExecutionTimeAsync(() => service.RetrieveAsync()); + var second = await ExecutionTimeAsync(() => service.RetrieveAsync()); + + // Assert + memoryCache.Verify( + cache => cache.TryGetValue(It.IsAny(), out value), + Times.Exactly(2) // first + second + ); + second.Should().BeLessThan(first); + } + + [Fact] + [Trait("Category", "Unit")] + public async Task GivenRetrieveByIdAsync_WhenRepositoryFindByIdAsyncReturnsNull_TheResultShouldBeNull() + { + // Arrange + var id = 999; + var (repository, logger, memoryCache) = PlayerMocks.SetupServiceMocks(); + repository.Setup(repository => repository.FindByIdAsync(id)).ReturnsAsync((Player?)null); + + var service = new PlayerService(repository.Object, logger.Object, memoryCache.Object); + + // Act + var result = await service.RetrieveByIdAsync(id); + + // Assert + repository.Verify(repository => repository.FindByIdAsync(It.IsAny()), Times.Once); + result.Should().BeNull(); + } + + [Fact] + [Trait("Category", "Unit")] + public async Task GivenRetrieveByIdAsync_WhenRepositoryFindByIdAsyncReturnsPlayer_TheResultShouldBePlayer() + { + // Arrange + var player = PlayerFakes.CreateOneByIdFromStarting11(9); + var (repository, logger, memoryCache) = PlayerMocks.SetupServiceMocks(); + repository.Setup(repository => repository.FindByIdAsync(player.Id)).ReturnsAsync(player); + + var service = new PlayerService(repository.Object, logger.Object, memoryCache.Object); + + // Act + var result = await service.RetrieveByIdAsync(player.Id); + + // Assert + result.Should().BeOfType(); + result.Should().BeEquivalentTo(player); + repository.Verify(repository => repository.FindByIdAsync(It.IsAny()), Times.Once); + } + + [Fact] + [Trait("Category", "Unit")] + public async Task GivenRetrieveBySquadNumberAsync_WhenRepositoryFindBySquadNumberAsyncReturnsNull_ThenResultShouldBeNull() + { + // Arrange + var squadNumber = 999; + var (repository, logger, memoryCache) = PlayerMocks.SetupServiceMocks(); + repository + .Setup(repository => repository.FindBySquadNumberAsync(squadNumber)) + .ReturnsAsync((Player?)null); + + var service = new PlayerService(repository.Object, logger.Object, memoryCache.Object); + + // Act + var result = await service.RetrieveBySquadNumberAsync(squadNumber); + + // Assert + repository.Verify( + repository => repository.FindBySquadNumberAsync(It.IsAny()), + Times.Once + ); + result.Should().BeNull(); + } + + [Fact] + [Trait("Category", "Unit")] + public async Task GivenRetrieveBySquadNumberAsync_WhenRepositoryFindBySquadNumberAsyncReturnsPlayer_ThenResultShouldBePlayer() + { + // Arrange + var player = PlayerFakes.CreateOneByIdFromStarting11(9); + var (repository, logger, memoryCache) = PlayerMocks.SetupServiceMocks(); + repository + .Setup(repository => repository.FindBySquadNumberAsync(player.SquadNumber)) + .ReturnsAsync(player); + + var service = new PlayerService(repository.Object, logger.Object, memoryCache.Object); + + // Act + var result = await service.RetrieveBySquadNumberAsync(player.SquadNumber); + + // Assert + repository.Verify( + repository => repository.FindBySquadNumberAsync(It.IsAny()), + Times.Once + ); + result.Should().BeOfType(); + result.Should().BeEquivalentTo(player); + } + + /* ------------------------------------------------------------------------- + * Update + * ---------------------------------------------------------------------- */ + + [Fact] + [Trait("Category", "Unit")] + public async Task GivenUpdateAsync_WhenRepositoryFindByIdAsyncReturnsPlayer_ThenRepositoryUpdateAsyncAndCacheRemove() + { + // Arrange + var player = PlayerFakes.CreateOneByIdFromStarting11(9); + var (repository, logger, memoryCache) = PlayerMocks.SetupServiceMocks(); + repository.Setup(repository => repository.FindByIdAsync(player.Id)).ReturnsAsync(player); + + var service = new PlayerService(repository.Object, logger.Object, memoryCache.Object); + + // Act + await service.UpdateAsync(player); + + // Assert + repository.Verify(repository => repository.FindByIdAsync(It.IsAny()), Times.Once); + repository.Verify(repository => repository.UpdateAsync(player), Times.Once); + memoryCache.Verify(cache => cache.Remove(It.IsAny()), Times.Once); + } + + /* ------------------------------------------------------------------------- + * Delete + * ---------------------------------------------------------------------- */ + + [Fact] + [Trait("Category", "Unit")] + public async Task GivenDeleteAsync_WhenRepositoryFindByIdAsyncReturnsPlayer_ThenRepositoryDeleteAsyncAndCacheRemove() + { + // Arrange + var player = PlayerFakes.CreateOneNew(); + var (repository, logger, memoryCache) = PlayerMocks.SetupServiceMocks(); + repository.Setup(repository => repository.FindByIdAsync(player.Id)).ReturnsAsync(player); + + var service = new PlayerService(repository.Object, logger.Object, memoryCache.Object); + + // Act + await service.DeleteAsync(player.Id); + + // Assert + repository.Verify(repository => repository.FindByIdAsync(It.IsAny()), Times.Once); + repository.Verify(repository => repository.RemoveAsync(It.IsAny()), Times.Once); + memoryCache.Verify(cache => cache.Remove(It.IsAny()), Times.Exactly(1)); + } + + private async Task ExecutionTimeAsync(Func awaitable) + { + var stopwatch = new Stopwatch(); + + stopwatch.Start(); + await awaitable(); + stopwatch.Stop(); + + return stopwatch.ElapsedMilliseconds; + } + + protected virtual void Dispose(bool disposing) + { + if (!_disposed) + { + Environment.SetEnvironmentVariable("ASPNETCORE_ENVIRONMENT", null); + _disposed = true; + } + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } +} diff --git a/Dotnet.Samples.AspNetCore.WebApi.Tests/Utilities/DatabaseFakes.cs b/Dotnet.Samples.AspNetCore.WebApi.Tests/Utilities/DatabaseFakes.cs new file mode 100644 index 0000000..a941681 --- /dev/null +++ b/Dotnet.Samples.AspNetCore.WebApi.Tests/Utilities/DatabaseFakes.cs @@ -0,0 +1,55 @@ +using System.Data.Common; +using Dotnet.Samples.AspNetCore.WebApi.Data; +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; + +namespace Dotnet.Samples.AspNetCore.WebApi.Tests.Utilities +{ + /// + /// A Fake is a working implementation that’s simpler than the real one. + /// It usually has some “real” logic but is not suitable for production + /// (e.g., an in‑memory database instead of a full SQL Server). Fakes are + /// useful when you need behavior that’s closer to reality but still want + /// to avoid external dependencies. + /// + public static class DatabaseFakes + { + public static (DbConnection, DbContextOptions) CreateSqliteConnection() + { + var dbConnection = new SqliteConnection("Filename=:memory:"); + dbConnection.Open(); + + var dbContextOptions = new DbContextOptionsBuilder() + .UseSqlite(dbConnection) + .Options; + + return (dbConnection, dbContextOptions); + } + + public static PlayerDbContext CreateDbContext( + DbContextOptions dbContextOptions + ) + { + return new PlayerDbContext(dbContextOptions); + } + + public static void CreateTable(this PlayerDbContext context) + { + using var cmd = context.Database.GetDbConnection().CreateCommand(); + cmd.CommandText = + @" + CREATE TABLE players ( + id INTEGER PRIMARY KEY, + firstName TEXT NOT NULL, + /* ... other columns ... */ + )"; + cmd.ExecuteNonQuery(); + } + + public static void Seed(this PlayerDbContext context) + { + context.Players.AddRange(PlayerFakes.CreateStarting11()); + context.SaveChanges(); + } + } +} diff --git a/Dotnet.Samples.AspNetCore.WebApi.Tests/Utilities/PlayerFakes.cs b/Dotnet.Samples.AspNetCore.WebApi.Tests/Utilities/PlayerFakes.cs index 0eddcbc..c5c1c44 100644 --- a/Dotnet.Samples.AspNetCore.WebApi.Tests/Utilities/PlayerFakes.cs +++ b/Dotnet.Samples.AspNetCore.WebApi.Tests/Utilities/PlayerFakes.cs @@ -2,8 +2,15 @@ using Dotnet.Samples.AspNetCore.WebApi.Enums; using Dotnet.Samples.AspNetCore.WebApi.Models; -namespace Dotnet.Samples.AspNetCore.WebApi.Tests; +namespace Dotnet.Samples.AspNetCore.WebApi.Tests.Utilities; +/// +/// A Fake is a working implementation that’s simpler than the real one. +/// It usually has some “real” logic but is not suitable for production +/// (e.g., an in‑memory database instead of a full SQL Server). Fakes are +/// useful when you need behavior that’s closer to reality but still want +/// to avoid external dependencies. +/// public static class PlayerFakes { public static Player CreateOneByIdFromStarting11(int id) diff --git a/Dotnet.Samples.AspNetCore.WebApi.Tests/Utilities/PlayerMocks.cs b/Dotnet.Samples.AspNetCore.WebApi.Tests/Utilities/PlayerMocks.cs index 8512db8..b01e594 100644 --- a/Dotnet.Samples.AspNetCore.WebApi.Tests/Utilities/PlayerMocks.cs +++ b/Dotnet.Samples.AspNetCore.WebApi.Tests/Utilities/PlayerMocks.cs @@ -1,13 +1,33 @@ +using Dotnet.Samples.AspNetCore.WebApi.Controllers; +using Dotnet.Samples.AspNetCore.WebApi.Data; +using Dotnet.Samples.AspNetCore.WebApi.Services; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Routing; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; using Moq; -namespace Dotnet.Samples.AspNetCore.WebApi.Tests +namespace Dotnet.Samples.AspNetCore.WebApi.Tests.Utilities { + /// + /// A Mock is a dynamic test double that not only provides responses but + /// also records interactions. Mocks are pre‑programmed with expectations + /// (like “this method should be called once with these parameters”) and can + /// later verify that the expected calls occurred. Frameworks such as Moq + /// make it easy to set up and verify these interactions. + /// public static class PlayerMocks { + public static Mock ServiceMock() + { + return new Mock(); + } + + public static Mock RepositoryMock() + { + return new Mock(); + } + public static Mock> LoggerMock() where T : class { @@ -39,5 +59,27 @@ public static Mock UrlHelperMock() return mock; } + + public static ( + Mock repository, + Mock> logger, + Mock memoryCache + ) SetupServiceMocks(object? cacheValue = null) + { + var repository = RepositoryMock(); + var logger = LoggerMock(); + var memoryCache = MemoryCacheMock(cacheValue ?? It.IsAny()); + return (repository, logger, memoryCache); + } + + public static ( + Mock service, + Mock> logger + ) SetupControllerMocks(object? cacheValue = null) + { + var service = ServiceMock(); + var logger = LoggerMock(); + return (service, logger); + } } } diff --git a/Dotnet.Samples.AspNetCore.WebApi.Tests/Utilities/PlayerStubs.cs b/Dotnet.Samples.AspNetCore.WebApi.Tests/Utilities/PlayerStubs.cs index 558224c..282eb17 100644 --- a/Dotnet.Samples.AspNetCore.WebApi.Tests/Utilities/PlayerStubs.cs +++ b/Dotnet.Samples.AspNetCore.WebApi.Tests/Utilities/PlayerStubs.cs @@ -4,60 +4,16 @@ using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; -namespace Dotnet.Samples.AspNetCore.WebApi.Tests +namespace Dotnet.Samples.AspNetCore.WebApi.Tests.Utilities { + /// + /// A Stub provides pre‑configured, hard‑coded responses to method calls. + /// Its purpose is simply to supply data to the system under test without + /// any behavior verification. For example, a Stub for a repository might + /// always return a specific user regardless of the input. + /// public static class PlayerStubs { - public static (DbConnection, DbContextOptions) CreateSqliteConnection() - { - var dbConnection = new SqliteConnection("Filename=:memory:"); - dbConnection.Open(); - - var dbContextOptions = new DbContextOptionsBuilder() - .UseSqlite(dbConnection) - .Options; - - return (dbConnection, dbContextOptions); - } - - public static void CreateTable(PlayerDbContext dbContext) - { - using var dbCommand = dbContext.Database.GetDbConnection().CreateCommand(); - - dbCommand.CommandText = - @" - CREATE TABLE IF NOT EXISTS players - ( - id INTEGER, - firstName TEXT NOT NULL, - middleName TEXT, - lastName TEXT NOT NULL, - dateOfBirth TEXT, - squadNumber INTEGER NOT NULL, - position TEXT NOT NULL, - abbrPosition TEXT, - team TEXT, - league TEXT, - starting11 BOOLEAN, - PRIMARY KEY(id) - );"; - - dbCommand.ExecuteNonQuery(); - } - - public static PlayerDbContext CreateDbContext( - DbContextOptions dbContextOptions - ) - { - return new PlayerDbContext(dbContextOptions); - } - - public static void SeedDbContext(PlayerDbContext dbContext) - { - dbContext.AddRange(PlayerFakes.CreateStarting11()); - dbContext.SaveChanges(); - } - public static ModelStateDictionary CreateModelError(string key, string errorMessage) { var modelStateDictionary = new ModelStateDictionary(); diff --git a/Dotnet.Samples.AspNetCore.WebApi.Tests/packages.lock.json b/Dotnet.Samples.AspNetCore.WebApi.Tests/packages.lock.json index 1f92337..0a12b00 100644 --- a/Dotnet.Samples.AspNetCore.WebApi.Tests/packages.lock.json +++ b/Dotnet.Samples.AspNetCore.WebApi.Tests/packages.lock.json @@ -1000,8 +1000,8 @@ }, "Microsoft.OpenApi": { "type": "Transitive", - "resolved": "1.6.22", - "contentHash": "aBvunmrdu/x+4CaA/UP1Jx4xWGwk4kymhoIRnn2Vp+zi5/KOPQJ9EkSXHRUr01WcGKtYl3Au7XfkPJbU1G2sjQ==" + "resolved": "1.6.23", + "contentHash": "tZ1I0KXnn98CWuV8cpI247A17jaY+ILS9vvF7yhI0uPPEqF4P1d7BWL5Uwtel10w9NucllHB3nTkfYTAcHAh8g==" }, "Microsoft.SqlServer.Server": { "type": "Transitive", @@ -1324,35 +1324,35 @@ }, "Swashbuckle.AspNetCore": { "type": "Transitive", - "resolved": "7.3.1", - "contentHash": "6u8w+UXp/sF89xQjfydWw6znQrUpbpFOmEIs8ODE+S0bV+mCQ9dNP4mk+HRsGHylpDaP5KSYSCEfFSgluLXHsA==", + "resolved": "8.0.0", + "contentHash": "K9FzGTxmwfD+7sVf/FTq/TZFHBTXcROgdcg7gLFwKwgvXwaqTtjGVdam27j0kYfgZZyWlOKr+abmtyd2nAd5eA==", "dependencies": { "Microsoft.Extensions.ApiDescription.Server": "6.0.5", - "Swashbuckle.AspNetCore.Swagger": "7.3.1", - "Swashbuckle.AspNetCore.SwaggerGen": "7.3.1", - "Swashbuckle.AspNetCore.SwaggerUI": "7.3.1" + "Swashbuckle.AspNetCore.Swagger": "8.0.0", + "Swashbuckle.AspNetCore.SwaggerGen": "8.0.0", + "Swashbuckle.AspNetCore.SwaggerUI": "8.0.0" } }, "Swashbuckle.AspNetCore.Swagger": { "type": "Transitive", - "resolved": "7.3.1", - "contentHash": "jQuJ8kVbq+YE8WsJE3RwWHlF1kasp0QkA9Gl6NeNLICrhcgN8IQIthMufYW6t/4hpcN5cBIdES5jCEV81WjHbA==", + "resolved": "8.0.0", + "contentHash": "+8Y4pVTWbnzotIk6d6rcwsHGpCchPDqqrvYkyGlI3go+pFaKM+4eX30iCyI0hvr0RMtObJCFhK6aDtlQFbEF1g==", "dependencies": { - "Microsoft.OpenApi": "1.6.22" + "Microsoft.OpenApi": "1.6.23" } }, "Swashbuckle.AspNetCore.SwaggerGen": { "type": "Transitive", - "resolved": "7.3.1", - "contentHash": "xs7Pznb3SSjZy7HpThE0ILqECfQFsGDHOrRoIYD/j67ktdRR1juDG4AMyidXXCOipgzHanZoF+nFrc+Nmjqjyw==", + "resolved": "8.0.0", + "contentHash": "skCeIQ93yMcUm1PQby5qitFM6KLIlLMj4/i8JHy86x2OFzxTNaaas2kUg6rNV3JvucFvYCNyImg7NMtZHErSzQ==", "dependencies": { - "Swashbuckle.AspNetCore.Swagger": "7.3.1" + "Swashbuckle.AspNetCore.Swagger": "8.0.0" } }, "Swashbuckle.AspNetCore.SwaggerUI": { "type": "Transitive", - "resolved": "7.3.1", - "contentHash": "hs6C+lmNEzipOA1WPQpIaGvvoXjUbnoevbv6l7o9ZQE8SNF8ggjOmK6NB6cYdMcEvk0uBeKl4Qq/BnRt5MFVqg==" + "resolved": "8.0.0", + "contentHash": "IMqmgclFiZL2QIfopOmWYofZzckrl+SdMt1h4mKC0jc94F+uzt3IHA3YFC0CGlwBqTTSnxHqNUKomNTeAhZbYA==" }, "System.ClientModel": { "type": "Transitive", @@ -1643,7 +1643,7 @@ "Microsoft.EntityFrameworkCore.SqlServer": "[9.0.3, )", "Microsoft.EntityFrameworkCore.Sqlite": "[9.0.3, )", "Microsoft.VisualStudio.Web.CodeGeneration.Design": "[9.0.0, )", - "Swashbuckle.AspNetCore": "[7.3.1, )" + "Swashbuckle.AspNetCore": "[8.0.0, )" } } } diff --git a/Dotnet.Samples.AspNetCore.WebApi/Controllers/PlayersController.cs b/Dotnet.Samples.AspNetCore.WebApi/Controllers/PlayerController.cs similarity index 94% rename from Dotnet.Samples.AspNetCore.WebApi/Controllers/PlayersController.cs rename to Dotnet.Samples.AspNetCore.WebApi/Controllers/PlayerController.cs index 8982bd9..4aa01ee 100644 --- a/Dotnet.Samples.AspNetCore.WebApi/Controllers/PlayersController.cs +++ b/Dotnet.Samples.AspNetCore.WebApi/Controllers/PlayerController.cs @@ -8,11 +8,11 @@ namespace Dotnet.Samples.AspNetCore.WebApi.Controllers; [ApiController] [Route("[controller]")] [Produces("application/json")] -public class PlayersController(IPlayerService playerService, ILogger logger) +public class PlayerController(IPlayerService playerService, ILogger logger) : ControllerBase { private readonly IPlayerService _playerService = playerService; - private readonly ILogger _logger = logger; + private readonly ILogger _logger = logger; /* ------------------------------------------------------------------------- * HTTP POST diff --git a/Dotnet.Samples.AspNetCore.WebApi/Data/IPlayerRepository.cs b/Dotnet.Samples.AspNetCore.WebApi/Data/IPlayerRepository.cs new file mode 100644 index 0000000..cb90fca --- /dev/null +++ b/Dotnet.Samples.AspNetCore.WebApi/Data/IPlayerRepository.cs @@ -0,0 +1,21 @@ +// IPlayerRepository.cs + +using Dotnet.Samples.AspNetCore.WebApi.Models; + +namespace Dotnet.Samples.AspNetCore.WebApi.Data; + +/// +/// Provides specialized repository operations for Player entities. +/// +public interface IPlayerRepository : IRepository +{ + /// + /// Finds a Player in the repository by their Squad Number. + /// + /// The Squad Number of the Player to retrieve. + /// + /// A ValueTask representing the asynchronous operation, containing the Player if found, + /// or null if no Player with the specified Squad Number exists. + /// + ValueTask FindBySquadNumberAsync(int squadNumber); +} diff --git a/Dotnet.Samples.AspNetCore.WebApi/Data/IRepository.cs b/Dotnet.Samples.AspNetCore.WebApi/Data/IRepository.cs new file mode 100644 index 0000000..612c885 --- /dev/null +++ b/Dotnet.Samples.AspNetCore.WebApi/Data/IRepository.cs @@ -0,0 +1,47 @@ +namespace Dotnet.Samples.AspNetCore.WebApi.Data; + +/// +/// Provides generic repository operations for entities of type . +/// +/// The entity type managed by this repository. +public interface IRepository + where T : class +{ + /// + /// Adds a new entity to the repository. + /// + /// The entity to create. + /// A Task representing the asynchronous operation. + Task AddAsync(T entity); + + /// + /// Gets all entities from the repository. + /// + /// A Task representing the asynchronous operation, + /// containing a list of all entities. + Task> GetAllAsync(); + + /// + /// Finds an entity on the repository by its unique identifier. + /// + /// The unique identifier of the entity to retrieve. + /// + /// A ValueTask representing the asynchronous operation, containing the entity if found, + /// or null if no entity with the specified ID exists. + /// + ValueTask FindByIdAsync(long id); + + /// + /// Updates an existing entity in the repository. + /// + /// The entity with updated values. + /// A Task representing the asynchronous operation. + Task UpdateAsync(T entity); + + /// + /// Removes an entity from the repository by its unique identifier. + /// + /// The unique identifier of the entity to remove. + /// A Task representing the asynchronous operation. + Task RemoveAsync(long id); +} diff --git a/Dotnet.Samples.AspNetCore.WebApi/Data/PlayerRepository.cs b/Dotnet.Samples.AspNetCore.WebApi/Data/PlayerRepository.cs new file mode 100644 index 0000000..0c577da --- /dev/null +++ b/Dotnet.Samples.AspNetCore.WebApi/Data/PlayerRepository.cs @@ -0,0 +1,12 @@ +using Dotnet.Samples.AspNetCore.WebApi.Models; +using Microsoft.EntityFrameworkCore; + +namespace Dotnet.Samples.AspNetCore.WebApi.Data; + +public sealed class PlayerRepository(PlayerDbContext dbContext) + : Repository(dbContext), + IPlayerRepository +{ + public async ValueTask FindBySquadNumberAsync(int squadNumber) => + await _dbSet.FirstOrDefaultAsync(p => p.SquadNumber == squadNumber); +} diff --git a/Dotnet.Samples.AspNetCore.WebApi/Data/Repository.cs b/Dotnet.Samples.AspNetCore.WebApi/Data/Repository.cs new file mode 100644 index 0000000..c097741 --- /dev/null +++ b/Dotnet.Samples.AspNetCore.WebApi/Data/Repository.cs @@ -0,0 +1,42 @@ +using Microsoft.EntityFrameworkCore; + +namespace Dotnet.Samples.AspNetCore.WebApi.Data; + +public class Repository : IRepository + where T : class +{ + protected readonly DbContext _dbContext; + protected readonly DbSet _dbSet; + + public Repository(DbContext dbContext) + { + _dbContext = dbContext; + _dbSet = _dbContext.Set(); + } + + public async Task AddAsync(T entity) + { + await _dbSet.AddAsync(entity); + await _dbContext.SaveChangesAsync(); + } + + public async Task> GetAllAsync() => await _dbSet.ToListAsync(); + + public async ValueTask FindByIdAsync(long id) => await _dbSet.FindAsync(id); + + public async Task UpdateAsync(T entity) + { + _dbSet.Update(entity); + await _dbContext.SaveChangesAsync(); + } + + public async Task RemoveAsync(long id) + { + var entity = await _dbSet.FindAsync(id); + if (entity != null) + { + _dbSet.Remove(entity); + await _dbContext.SaveChangesAsync(); + } + } +} diff --git a/Dotnet.Samples.AspNetCore.WebApi/Program.cs b/Dotnet.Samples.AspNetCore.WebApi/Program.cs index d5d2817..ed46fa0 100644 --- a/Dotnet.Samples.AspNetCore.WebApi/Program.cs +++ b/Dotnet.Samples.AspNetCore.WebApi/Program.cs @@ -36,6 +36,10 @@ } }); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddMemoryCache(); + if (builder.Environment.IsDevelopment()) { builder.Services.AddSwaggerGen(options => @@ -66,9 +70,6 @@ }); } -builder.Services.AddScoped(); -builder.Services.AddMemoryCache(); - var app = builder.Build(); /* ----------------------------------------------------------------------------- diff --git a/Dotnet.Samples.AspNetCore.WebApi/Services/IPlayerService.cs b/Dotnet.Samples.AspNetCore.WebApi/Services/IPlayerService.cs index 0828558..434efa8 100644 --- a/Dotnet.Samples.AspNetCore.WebApi/Services/IPlayerService.cs +++ b/Dotnet.Samples.AspNetCore.WebApi/Services/IPlayerService.cs @@ -3,26 +3,26 @@ namespace Dotnet.Samples.AspNetCore.WebApi.Services { /// - /// Interface for managing Player entities in the database context. + /// Interface for managing Player entities in the repository. /// public interface IPlayerService { /// - /// Adds a new Player to the database context. + /// Adds a new Player to the repository. /// /// The Player to create. /// A Task representing the asynchronous operation. public Task CreateAsync(Player player); /// - /// Retrieves all players from the database context. + /// Retrieves all players from the repository. /// /// A Task representing the asynchronous operation, /// containing a list of all players. public Task> RetrieveAsync(); /// - /// Retrieves a Player from the database context by its ID. + /// Retrieves a Player from the repository by its ID. /// /// The ID of the Player to retrieve. /// @@ -32,7 +32,7 @@ public interface IPlayerService public ValueTask RetrieveByIdAsync(long id); /// - /// Retrieves a Player from the database context by its Squad Number. + /// Retrieves a Player from the repository by its Squad Number. /// /// The Squad Number of the Player to retrieve. /// @@ -42,14 +42,14 @@ public interface IPlayerService public ValueTask RetrieveBySquadNumberAsync(int squadNumber); /// - /// Updates (entirely) an existing Player in the database context. + /// Updates (entirely) an existing Player in the repository. /// /// The Player to update. /// A Task representing the asynchronous operation. public Task UpdateAsync(Player player); /// - /// Removes an existing Player from the database context. + /// Removes an existing Player from the repository. /// /// The ID of the Player to delete. /// A Task representing the asynchronous operation. diff --git a/Dotnet.Samples.AspNetCore.WebApi/Services/PlayerService.cs b/Dotnet.Samples.AspNetCore.WebApi/Services/PlayerService.cs index e4fb712..17e2377 100644 --- a/Dotnet.Samples.AspNetCore.WebApi/Services/PlayerService.cs +++ b/Dotnet.Samples.AspNetCore.WebApi/Services/PlayerService.cs @@ -6,7 +6,7 @@ namespace Dotnet.Samples.AspNetCore.WebApi.Services; public class PlayerService( - PlayerDbContext dbContext, + IPlayerRepository playerRepository, ILogger logger, IMemoryCache memoryCache ) : IPlayerService @@ -14,7 +14,7 @@ IMemoryCache memoryCache private const string MemoryCache_Key_RetrieveAsync = "MemoryCache_Key_RetrieveAsync"; private const string AspNetCore_Environment = "ASPNETCORE_ENVIRONMENT"; private const string Development = "Development"; - private readonly PlayerDbContext _dbContext = dbContext; + private readonly IPlayerRepository _playerRepository = playerRepository; private readonly ILogger _logger = logger; private readonly IMemoryCache _memoryCache = memoryCache; @@ -24,8 +24,7 @@ IMemoryCache memoryCache public async Task CreateAsync(Player player) { - _dbContext.Add(player); - await _dbContext.SaveChangesAsync(); + await _playerRepository.AddAsync(player); _memoryCache.Remove(MemoryCache_Key_RetrieveAsync); } @@ -53,7 +52,7 @@ Use multiple environments in ASP.NET Core await Task.Delay(new Random().Next(2600, 4200)); } - players = await _dbContext.Players.ToListAsync(); + players = await _playerRepository.GetAllAsync(); _logger.Log(LogLevel.Information, "Players retrieved from DbContext."); using (var cacheEntry = _memoryCache.CreateEntry(MemoryCache_Key_RetrieveAsync)) @@ -66,17 +65,11 @@ Use multiple environments in ASP.NET Core } } - public async ValueTask RetrieveByIdAsync(long id) - { - return await _dbContext.Players.FindAsync(id); - } + public async ValueTask RetrieveByIdAsync(long id) => + await _playerRepository.FindByIdAsync(id); - public async ValueTask RetrieveBySquadNumberAsync(int squadNumber) - { - return await _dbContext.Players.FirstOrDefaultAsync(player => - player.SquadNumber == squadNumber - ); - } + public async ValueTask RetrieveBySquadNumberAsync(int squadNumber) => + await _playerRepository.FindBySquadNumberAsync(squadNumber); /* ------------------------------------------------------------------------- * Update @@ -84,10 +77,10 @@ Use multiple environments in ASP.NET Core public async Task UpdateAsync(Player player) { - if (await _dbContext.Players.FindAsync(player.Id) is Player entity) + if (await _playerRepository.FindByIdAsync(player.Id) is Player entity) { entity.MapFrom(player); - await _dbContext.SaveChangesAsync(); + await _playerRepository.UpdateAsync(entity); _memoryCache.Remove(MemoryCache_Key_RetrieveAsync); } } @@ -98,10 +91,9 @@ public async Task UpdateAsync(Player player) public async Task DeleteAsync(long id) { - if (await _dbContext.Players.FindAsync(id) is Player entity) + if (await _playerRepository.FindByIdAsync(id) is not null) { - _dbContext.Remove(entity); - await _dbContext.SaveChangesAsync(); + await _playerRepository.RemoveAsync(id); _memoryCache.Remove(MemoryCache_Key_RetrieveAsync); } }