diff --git a/CleanArchitecture.Api/Controllers/UserController.cs b/CleanArchitecture.Api/Controllers/UserController.cs index a1dc752..791145d 100644 --- a/CleanArchitecture.Api/Controllers/UserController.cs +++ b/CleanArchitecture.Api/Controllers/UserController.cs @@ -21,9 +21,10 @@ public class UserController : ApiController } [HttpGet] - public string GetAllUsersAsync() + public async Task GetAllUsersAsync() { - return "test"; + var users = await _userService.GetAllUsersAsync(); + return Response(users); } [HttpGet("{id}")] diff --git a/CleanArchitecture.Api/Program.cs b/CleanArchitecture.Api/Program.cs index 06f6b4e..db6a81b 100644 --- a/CleanArchitecture.Api/Program.cs +++ b/CleanArchitecture.Api/Program.cs @@ -1,4 +1,5 @@ using CleanArchitecture.Application.Extensions; +using CleanArchitecture.Domain.Extensions; using CleanArchitecture.Infrastructure.Database; using CleanArchitecture.Infrastructure.Extensions; using Microsoft.AspNetCore.Builder; @@ -24,6 +25,8 @@ builder.Services.AddDbContext(options => builder.Services.AddInfrastructure(); builder.Services.AddQueryHandlers(); builder.Services.AddServices(); +builder.Services.AddCommandHandlers(); +builder.Services.AddNotificationHandlers(); builder.Services.AddMediatR(cfg => { diff --git a/CleanArchitecture.Application.Tests/CleanArchitecture.Application.Tests.csproj b/CleanArchitecture.Application.Tests/CleanArchitecture.Application.Tests.csproj index e3e890e..0b18234 100644 --- a/CleanArchitecture.Application.Tests/CleanArchitecture.Application.Tests.csproj +++ b/CleanArchitecture.Application.Tests/CleanArchitecture.Application.Tests.csproj @@ -9,8 +9,9 @@ - - + + + runtime; build; native; contentfiles; analyzers; buildtransitive all @@ -21,4 +22,9 @@ + + + + + diff --git a/CleanArchitecture.Application.Tests/Fixtures/Queries/QueryHandlerBaseFixture.cs b/CleanArchitecture.Application.Tests/Fixtures/Queries/QueryHandlerBaseFixture.cs new file mode 100644 index 0000000..204e29c --- /dev/null +++ b/CleanArchitecture.Application.Tests/Fixtures/Queries/QueryHandlerBaseFixture.cs @@ -0,0 +1,33 @@ +using CleanArchitecture.Domain.Interfaces; +using CleanArchitecture.Domain.Notifications; +using Moq; + +namespace CleanArchitecture.Application.Tests.Fixtures.Queries; + +public class QueryHandlerBaseFixture +{ + public Mock Bus { get; } = new(); + + public QueryHandlerBaseFixture VerifyExistingNotification(string key, string errorCode, string message) + { + Bus.Verify( + bus => bus.RaiseEventAsync( + It.Is( + notification => + notification.Key == key && + notification.Code == errorCode && + notification.Value == message)), + Times.Once); + + return this; + } + + public QueryHandlerBaseFixture VerifyNoDomainNotification() + { + Bus.Verify( + bus => bus.RaiseEventAsync(It.IsAny()), + Times.Never); + + return this; + } +} \ No newline at end of file diff --git a/CleanArchitecture.Application.Tests/Fixtures/Queries/Users/GetAllUsersTestFixture.cs b/CleanArchitecture.Application.Tests/Fixtures/Queries/Users/GetAllUsersTestFixture.cs new file mode 100644 index 0000000..a9b9e5d --- /dev/null +++ b/CleanArchitecture.Application.Tests/Fixtures/Queries/Users/GetAllUsersTestFixture.cs @@ -0,0 +1,18 @@ +using CleanArchitecture.Application.Queries.Users.GetAll; +using CleanArchitecture.Domain.Interfaces.Repositories; +using Moq; + +namespace CleanArchitecture.Application.Tests.Fixtures.Queries.Users; + +public sealed class GetAllUsersTestFixture : QueryHandlerBaseFixture +{ + public Mock UserRepository { get; } + public GetAllUsersQueryHandler Handler { get; } + + public GetAllUsersTestFixture() + { + UserRepository = new(); + + Handler = new(UserRepository.Object); + } +} \ No newline at end of file diff --git a/CleanArchitecture.Application.Tests/Fixtures/Queries/Users/GetUserByIdTestFixture.cs b/CleanArchitecture.Application.Tests/Fixtures/Queries/Users/GetUserByIdTestFixture.cs new file mode 100644 index 0000000..8d110f3 --- /dev/null +++ b/CleanArchitecture.Application.Tests/Fixtures/Queries/Users/GetUserByIdTestFixture.cs @@ -0,0 +1,20 @@ +using CleanArchitecture.Application.Queries.Users.GetAll; +using CleanArchitecture.Application.Queries.Users.GetUserById; +using CleanArchitecture.Domain.Interfaces; +using CleanArchitecture.Domain.Interfaces.Repositories; +using Moq; + +namespace CleanArchitecture.Application.Tests.Fixtures.Queries.Users; + +public sealed class GetUserByIdTestFixture : QueryHandlerBaseFixture +{ + public Mock UserRepository { get; } + public GetUserByIdQueryHandler Handler { get; } + + public GetUserByIdTestFixture() + { + UserRepository = new(); + + Handler = new(UserRepository.Object, Bus.Object); + } +} \ No newline at end of file diff --git a/CleanArchitecture.Application.Tests/Fixtures/Services/UserServiceTestFixture.cs b/CleanArchitecture.Application.Tests/Fixtures/Services/UserServiceTestFixture.cs new file mode 100644 index 0000000..98ebbd3 --- /dev/null +++ b/CleanArchitecture.Application.Tests/Fixtures/Services/UserServiceTestFixture.cs @@ -0,0 +1,6 @@ +namespace CleanArchitecture.Application.Tests.Fixtures.Services; + +public sealed class UserServiceTestFixture +{ + +} \ No newline at end of file diff --git a/CleanArchitecture.Application.Tests/Queries/Users/GetAllUsersQueryHandlerTests.cs b/CleanArchitecture.Application.Tests/Queries/Users/GetAllUsersQueryHandlerTests.cs new file mode 100644 index 0000000..98aec9a --- /dev/null +++ b/CleanArchitecture.Application.Tests/Queries/Users/GetAllUsersQueryHandlerTests.cs @@ -0,0 +1,6 @@ +namespace CleanArchitecture.Application.Tests.Queries.Users; + +public sealed class GetAllUsersQueryHandlerTests +{ + +} \ No newline at end of file diff --git a/CleanArchitecture.Application.Tests/Queries/Users/GetUserByIdQueryHandlerTests.cs b/CleanArchitecture.Application.Tests/Queries/Users/GetUserByIdQueryHandlerTests.cs new file mode 100644 index 0000000..045d8b6 --- /dev/null +++ b/CleanArchitecture.Application.Tests/Queries/Users/GetUserByIdQueryHandlerTests.cs @@ -0,0 +1,6 @@ +namespace CleanArchitecture.Application.Tests.Queries.Users; + +public sealed class GetUserByIdQueryHandlerTests +{ + +} \ No newline at end of file diff --git a/CleanArchitecture.Application.Tests/Services/UserServiceTests.cs b/CleanArchitecture.Application.Tests/Services/UserServiceTests.cs new file mode 100644 index 0000000..1e86b71 --- /dev/null +++ b/CleanArchitecture.Application.Tests/Services/UserServiceTests.cs @@ -0,0 +1,6 @@ +namespace CleanArchitecture.Application.Tests.Services; + +public sealed class UserServiceTests +{ + +} \ No newline at end of file diff --git a/CleanArchitecture.Application.Tests/UnitTest1.cs b/CleanArchitecture.Application.Tests/UnitTest1.cs deleted file mode 100644 index 4f8971e..0000000 --- a/CleanArchitecture.Application.Tests/UnitTest1.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace CleanArchitecture.Application.Tests; - -public class UnitTest1 -{ - [Fact] - public void Test1() - { - } -} \ No newline at end of file diff --git a/CleanArchitecture.Application.Tests/Usings.cs b/CleanArchitecture.Application.Tests/Usings.cs deleted file mode 100644 index 8c927eb..0000000 --- a/CleanArchitecture.Application.Tests/Usings.cs +++ /dev/null @@ -1 +0,0 @@ -global using Xunit; \ No newline at end of file diff --git a/CleanArchitecture.Application/CleanArchitecture.Application.csproj b/CleanArchitecture.Application/CleanArchitecture.Application.csproj index 347d094..fd95266 100644 --- a/CleanArchitecture.Application/CleanArchitecture.Application.csproj +++ b/CleanArchitecture.Application/CleanArchitecture.Application.csproj @@ -7,6 +7,7 @@ + diff --git a/CleanArchitecture.Application/Extensions/ServiceCollectionExtension.cs b/CleanArchitecture.Application/Extensions/ServiceCollectionExtension.cs index 811bca4..e4ffb13 100644 --- a/CleanArchitecture.Application/Extensions/ServiceCollectionExtension.cs +++ b/CleanArchitecture.Application/Extensions/ServiceCollectionExtension.cs @@ -1,4 +1,6 @@ +using System.Collections.Generic; using CleanArchitecture.Application.Interfaces; +using CleanArchitecture.Application.Queries.Users.GetAll; using CleanArchitecture.Application.Queries.Users.GetUserById; using CleanArchitecture.Application.Services; using CleanArchitecture.Application.ViewModels; @@ -19,6 +21,7 @@ public static class ServiceCollectionExtension public static IServiceCollection AddQueryHandlers(this IServiceCollection services) { services.AddScoped, GetUserByIdQueryHandler>(); + services.AddScoped>, GetAllUsersQueryHandler>(); return services; } diff --git a/CleanArchitecture.Application/Interfaces/IUserService.cs b/CleanArchitecture.Application/Interfaces/IUserService.cs index 996e6d1..243970a 100644 --- a/CleanArchitecture.Application/Interfaces/IUserService.cs +++ b/CleanArchitecture.Application/Interfaces/IUserService.cs @@ -1,10 +1,12 @@ using CleanArchitecture.Application.ViewModels; using System.Threading.Tasks; using System; +using System.Collections.Generic; namespace CleanArchitecture.Application.Interfaces; public interface IUserService { public Task GetUserByUserIdAsync(Guid userId); + public Task> GetAllUsersAsync(); } \ No newline at end of file diff --git a/CleanArchitecture.Application/Queries/Users/GetAll/GetAllUsersQuery.cs b/CleanArchitecture.Application/Queries/Users/GetAll/GetAllUsersQuery.cs new file mode 100644 index 0000000..8af4dda --- /dev/null +++ b/CleanArchitecture.Application/Queries/Users/GetAll/GetAllUsersQuery.cs @@ -0,0 +1,7 @@ +using System.Collections.Generic; +using CleanArchitecture.Application.ViewModels; +using MediatR; + +namespace CleanArchitecture.Application.Queries.Users.GetAll; + +public sealed record GetAllUsersQuery() : IRequest>; diff --git a/CleanArchitecture.Application/Queries/Users/GetAll/GetAllUsersQueryHandler.cs b/CleanArchitecture.Application/Queries/Users/GetAll/GetAllUsersQueryHandler.cs new file mode 100644 index 0000000..d0a3b87 --- /dev/null +++ b/CleanArchitecture.Application/Queries/Users/GetAll/GetAllUsersQueryHandler.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using CleanArchitecture.Application.ViewModels; +using CleanArchitecture.Domain.Interfaces.Repositories; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace CleanArchitecture.Application.Queries.Users.GetAll; + +public sealed class GetAllUsersQueryHandler : + IRequestHandler> +{ + private readonly IUserRepository _userRepository; + + public GetAllUsersQueryHandler(IUserRepository userRepository) + { + _userRepository = userRepository; + } + + public async Task> Handle(GetAllUsersQuery request, CancellationToken cancellationToken) + { + return await _userRepository + .GetAllNoTracking() + .Select(x => UserViewModel.FromUser(x)) + .ToListAsync(cancellationToken); + } +} \ No newline at end of file diff --git a/CleanArchitecture.Application/Services/UserService.cs b/CleanArchitecture.Application/Services/UserService.cs index 61b3687..0b85778 100644 --- a/CleanArchitecture.Application/Services/UserService.cs +++ b/CleanArchitecture.Application/Services/UserService.cs @@ -1,6 +1,8 @@ using System; +using System.Collections.Generic; using System.Threading.Tasks; using CleanArchitecture.Application.Interfaces; +using CleanArchitecture.Application.Queries.Users.GetAll; using CleanArchitecture.Application.Queries.Users.GetUserById; using CleanArchitecture.Application.ViewModels; using CleanArchitecture.Domain.Interfaces; @@ -20,4 +22,9 @@ public sealed class UserService : IUserService { return await _bus.QueryAsync(new GetUserByIdQuery(userId)); } + + public async Task> GetAllUsersAsync() + { + return await _bus.QueryAsync(new GetAllUsersQuery()); + } } \ No newline at end of file diff --git a/CleanArchitecture.Domain.Tests/UnitTest1.cs b/CleanArchitecture.Domain.Tests/UnitTest1.cs deleted file mode 100644 index 88b57e0..0000000 --- a/CleanArchitecture.Domain.Tests/UnitTest1.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Xunit; - -namespace CleanArchitecture.Domain.Tests; - -public class UnitTest1 -{ - [Fact] - public void Test1() - { - } -} \ No newline at end of file diff --git a/CleanArchitecture.Domain/CleanArchitecture.Domain.csproj b/CleanArchitecture.Domain/CleanArchitecture.Domain.csproj index 8c0626a..847601c 100644 --- a/CleanArchitecture.Domain/CleanArchitecture.Domain.csproj +++ b/CleanArchitecture.Domain/CleanArchitecture.Domain.csproj @@ -10,4 +10,9 @@ + + + + + diff --git a/CleanArchitecture.Domain/Commands/CommandHandlerBase.cs b/CleanArchitecture.Domain/Commands/CommandHandlerBase.cs new file mode 100644 index 0000000..fe9b36c --- /dev/null +++ b/CleanArchitecture.Domain/Commands/CommandHandlerBase.cs @@ -0,0 +1,82 @@ +using System; +using System.Threading.Tasks; +using CleanArchitecture.Domain.Errors; +using CleanArchitecture.Domain.Interfaces; +using CleanArchitecture.Domain.Notifications; +using MediatR; + +namespace CleanArchitecture.Domain.Commands; + +public abstract class CommandHandlerBase +{ + protected readonly IMediatorHandler _bus; + private readonly IUnitOfWork _unitOfWork; + private readonly DomainNotificationHandler _notifications; + + protected CommandHandlerBase( + IMediatorHandler bus, + IUnitOfWork unitOfWork, + INotificationHandler notifications) + { + _bus = bus; + _unitOfWork = unitOfWork; + _notifications = (DomainNotificationHandler)notifications; + } + + public async Task CommitAsync() + { + if (_notifications.HasNotifications()) + { + return false; + } + + if (await _unitOfWork.CommitAsync()) + { + return true; + } + + await _bus.RaiseEventAsync( + new DomainNotification( + "Commit", + "Problem occured while saving the data. Please try again.", + ErrorCodes.CommitFailed)); + + return false; + } + + protected async Task NotifyAsync(string key, string message, string code) + { + await _bus.RaiseEventAsync( + new DomainNotification(key, message, code)); + } + + protected async Task NotifyAsync(DomainNotification notification) + { + await _bus.RaiseEventAsync(notification); + } + + protected async ValueTask TestValidityAsync(CommandBase command) + { + if (command.IsValid()) + { + return true; + } + + if (command.ValidationResult == null) + { + throw new InvalidOperationException("Command is invalid and should therefore have a validation result"); + } + + foreach (var error in command.ValidationResult!.Errors) + { + await NotifyAsync( + new DomainNotification( + command.MessageType, + error.ErrorMessage, + error.ErrorCode, + error.FormattedMessagePlaceholderValues)); + } + + return false; + } +} \ No newline at end of file diff --git a/CleanArchitecture.Domain/Commands/Users/DeleteUser/DeleteUserCommand.cs b/CleanArchitecture.Domain/Commands/Users/DeleteUser/DeleteUserCommand.cs new file mode 100644 index 0000000..34277b1 --- /dev/null +++ b/CleanArchitecture.Domain/Commands/Users/DeleteUser/DeleteUserCommand.cs @@ -0,0 +1,21 @@ +using System; + +namespace CleanArchitecture.Domain.Commands.Users.DeleteUser; + +public sealed class DeleteUserCommand : CommandBase +{ + private static readonly DeleteUserCommandValidation _validation = new(); + + public Guid UserId { get; } + + public DeleteUserCommand(Guid userId) : base(userId) + { + UserId = userId; + } + + public override bool IsValid() + { + ValidationResult = _validation.Validate(this); + return ValidationResult.IsValid; + } +} \ No newline at end of file diff --git a/CleanArchitecture.Domain/Commands/Users/DeleteUser/DeleteUserCommandHandler.cs b/CleanArchitecture.Domain/Commands/Users/DeleteUser/DeleteUserCommandHandler.cs new file mode 100644 index 0000000..b7abb33 --- /dev/null +++ b/CleanArchitecture.Domain/Commands/Users/DeleteUser/DeleteUserCommandHandler.cs @@ -0,0 +1,53 @@ +using System.Threading; +using System.Threading.Tasks; +using CleanArchitecture.Domain.Errors; +using CleanArchitecture.Domain.Events.User; +using CleanArchitecture.Domain.Interfaces; +using CleanArchitecture.Domain.Interfaces.Repositories; +using CleanArchitecture.Domain.Notifications; +using MediatR; + +namespace CleanArchitecture.Domain.Commands.Users.DeleteUser; + +public sealed class DeleteUserCommandHandler : CommandHandlerBase, + IRequestHandler +{ + private readonly IUserRepository _userRepository; + + public DeleteUserCommandHandler( + IMediatorHandler bus, + IUnitOfWork unitOfWork, + INotificationHandler notifications, + IUserRepository userRepository) : base(bus, unitOfWork, notifications) + { + _userRepository = userRepository; + } + + public async Task Handle(DeleteUserCommand request, CancellationToken cancellationToken) + { + if (!await TestValidityAsync(request)) + { + return; + } + + var user = await _userRepository.GetByIdAsync(request.UserId); + + if (user == null) + { + await NotifyAsync( + new DomainNotification( + request.MessageType, + $"There is no User with Id {request.UserId}", + ErrorCodes.ObjectNotFound)); + + return; + } + + _userRepository.Remove(user); + + if (!await CommitAsync()) + { + await _bus.RaiseEventAsync(new UserDeletedEvent(request.UserId)); + } + } +} \ No newline at end of file diff --git a/CleanArchitecture.Domain/Commands/Users/DeleteUser/DeleteUserCommandValidation.cs b/CleanArchitecture.Domain/Commands/Users/DeleteUser/DeleteUserCommandValidation.cs new file mode 100644 index 0000000..99dc44a --- /dev/null +++ b/CleanArchitecture.Domain/Commands/Users/DeleteUser/DeleteUserCommandValidation.cs @@ -0,0 +1,20 @@ +using CleanArchitecture.Domain.Errors; +using FluentValidation; + +namespace CleanArchitecture.Domain.Commands.Users.DeleteUser; + +public sealed class DeleteUserCommandValidation : AbstractValidator +{ + public DeleteUserCommandValidation() + { + AddRuleForId(); + } + + private void AddRuleForId() + { + RuleFor(cmd => cmd.UserId) + .NotEmpty() + .WithErrorCode(DomainErrorCodes.UserEmptyId) + .WithMessage("User id may not be empty"); + } +} \ No newline at end of file diff --git a/CleanArchitecture.Domain/Errors/DomainErrorCodes.cs b/CleanArchitecture.Domain/Errors/DomainErrorCodes.cs new file mode 100644 index 0000000..84d97c8 --- /dev/null +++ b/CleanArchitecture.Domain/Errors/DomainErrorCodes.cs @@ -0,0 +1,6 @@ +namespace CleanArchitecture.Domain.Errors; + +public static class DomainErrorCodes +{ + public const string UserEmptyId = "USER_EMPTY_ID"; +} \ No newline at end of file diff --git a/CleanArchitecture.Domain/EventHandler/UserEventHandler.cs b/CleanArchitecture.Domain/EventHandler/UserEventHandler.cs new file mode 100644 index 0000000..b592693 --- /dev/null +++ b/CleanArchitecture.Domain/EventHandler/UserEventHandler.cs @@ -0,0 +1,15 @@ +using System.Threading; +using System.Threading.Tasks; +using CleanArchitecture.Domain.Events.User; +using MediatR; + +namespace CleanArchitecture.Domain.EventHandler; + +public sealed class UserEventHandler : + INotificationHandler +{ + public Task Handle(UserDeletedEvent notification, CancellationToken cancellationToken) + { + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/CleanArchitecture.Domain/Events/User/UserDeletedEvent.cs b/CleanArchitecture.Domain/Events/User/UserDeletedEvent.cs new file mode 100644 index 0000000..576bcbf --- /dev/null +++ b/CleanArchitecture.Domain/Events/User/UserDeletedEvent.cs @@ -0,0 +1,13 @@ +using System; + +namespace CleanArchitecture.Domain.Events.User; + +public sealed class UserDeletedEvent : DomainEvent +{ + public Guid UserId { get; } + + public UserDeletedEvent(Guid userId) : base(userId) + { + UserId = userId; + } +} \ No newline at end of file diff --git a/CleanArchitecture.Domain/Extensions/ServiceCollectionExtension.cs b/CleanArchitecture.Domain/Extensions/ServiceCollectionExtension.cs new file mode 100644 index 0000000..26cd510 --- /dev/null +++ b/CleanArchitecture.Domain/Extensions/ServiceCollectionExtension.cs @@ -0,0 +1,26 @@ +using CleanArchitecture.Domain.Commands.Users.DeleteUser; +using CleanArchitecture.Domain.EventHandler; +using CleanArchitecture.Domain.Events.User; +using MediatR; +using Microsoft.Extensions.DependencyInjection; + +namespace CleanArchitecture.Domain.Extensions; + +public static class ServiceCollectionExtension +{ + public static IServiceCollection AddCommandHandlers(this IServiceCollection services) + { + // User + services.AddScoped, DeleteUserCommandHandler>(); + + return services; + } + + public static IServiceCollection AddNotificationHandlers(this IServiceCollection services) + { + // User + services.AddScoped, UserEventHandler>(); + + return services; + } +} \ No newline at end of file diff --git a/CleanArchitecture.Domain/Interfaces/Repositories/IRepository.cs b/CleanArchitecture.Domain/Interfaces/Repositories/IRepository.cs index 126ee6d..7c162bd 100644 --- a/CleanArchitecture.Domain/Interfaces/Repositories/IRepository.cs +++ b/CleanArchitecture.Domain/Interfaces/Repositories/IRepository.cs @@ -21,4 +21,5 @@ public interface IRepository : IDisposable where TEntity : Entity void Update(TEntity entity); Task ExistsAsync(Guid id); + public void Remove(TEntity entity, bool hardDelete = false); } \ No newline at end of file diff --git a/CleanArchitecture.Infrastructure.Tests/CleanArchitecture.Infrastructure.Tests.csproj b/CleanArchitecture.Infrastructure.Tests/CleanArchitecture.Infrastructure.Tests.csproj index e3e890e..1097e66 100644 --- a/CleanArchitecture.Infrastructure.Tests/CleanArchitecture.Infrastructure.Tests.csproj +++ b/CleanArchitecture.Infrastructure.Tests/CleanArchitecture.Infrastructure.Tests.csproj @@ -9,8 +9,10 @@ - - + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all @@ -21,4 +23,8 @@ + + + + diff --git a/CleanArchitecture.Infrastructure.Tests/Fixtures/UnitOfWorkTestFixture.cs b/CleanArchitecture.Infrastructure.Tests/Fixtures/UnitOfWorkTestFixture.cs new file mode 100644 index 0000000..9811b07 --- /dev/null +++ b/CleanArchitecture.Infrastructure.Tests/Fixtures/UnitOfWorkTestFixture.cs @@ -0,0 +1,16 @@ +using CleanArchitecture.Infrastructure.Database; +using Microsoft.Extensions.Logging; + +namespace CleanArchitecture.Infrastructure.Tests.Fixtures; + +public static class UnitOfWorkTestFixture +{ + public static UnitOfWork GetUnitOfWork( + ApplicationDbContext dbContext, + ILogger> logger) + { + var unitOfWork = new UnitOfWork(dbContext, logger); + + return unitOfWork; + } +} \ No newline at end of file diff --git a/CleanArchitecture.Infrastructure.Tests/InMemoryBusTests.cs b/CleanArchitecture.Infrastructure.Tests/InMemoryBusTests.cs new file mode 100644 index 0000000..7dc2567 --- /dev/null +++ b/CleanArchitecture.Infrastructure.Tests/InMemoryBusTests.cs @@ -0,0 +1,57 @@ +using CleanArchitecture.Domain.Commands.Users.DeleteUser; +using CleanArchitecture.Domain.Events.User; +using CleanArchitecture.Domain.Notifications; +using MediatR; +using Moq; +using Xunit; + +namespace CleanArchitecture.Infrastructure.Tests; + +public sealed class InMemoryBusTests +{ + [Fact] + public async Task InMemoryBus_Should_Publish_When_Not_DomainNotification() + { + var mediator = new Mock(); + + var inMemoryBus = new InMemoryBus(mediator.Object); + + var key = "Key"; + var value = "Value"; + var code = "Code"; + + var domainEvent = new DomainNotification(key, value, code); + + await inMemoryBus.RaiseEventAsync(domainEvent); + + mediator.Verify(x => x.Publish(domainEvent, CancellationToken.None), Times.Once); + } + + [Fact] + public async Task InMemoryBus_Should_Save_And_Publish_When_DomainNotification() + { + var mediator = new Mock(); + + var inMemoryBus = new InMemoryBus(mediator.Object); + + var userDeletedEvent = new UserDeletedEvent(Guid.NewGuid()); + + await inMemoryBus.RaiseEventAsync(userDeletedEvent); + + mediator.Verify(x => x.Publish(userDeletedEvent, CancellationToken.None), Times.Once); + } + + [Fact] + public async Task InMemoryBus_Should_Send_Command_Async() + { + var mediator = new Mock(); + + var inMemoryBus = new InMemoryBus(mediator.Object); + + var deleteUserCommand = new DeleteUserCommand(Guid.NewGuid()); + + await inMemoryBus.SendCommandAsync(deleteUserCommand); + + mediator.Verify(x => x.Send(deleteUserCommand, CancellationToken.None), Times.Once); + } +} \ No newline at end of file diff --git a/CleanArchitecture.Infrastructure.Tests/UnitOfWorkTests.cs b/CleanArchitecture.Infrastructure.Tests/UnitOfWorkTests.cs new file mode 100644 index 0000000..32f15da --- /dev/null +++ b/CleanArchitecture.Infrastructure.Tests/UnitOfWorkTests.cs @@ -0,0 +1,66 @@ +using CleanArchitecture.Infrastructure.Database; +using CleanArchitecture.Infrastructure.Tests.Fixtures; +using FluentAssertions; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using Moq; +using Xunit; + +namespace CleanArchitecture.Infrastructure.Tests; + +public sealed class UnitOfWorkTests +{ + [Fact] + public async Task Should_Commit_Async_Returns_True() + { + var options = new DbContextOptionsBuilder(); + var dbContextMock = new Mock(options.Options); + var loggerMock = new Mock>>(); + + dbContextMock + .Setup(x => x.SaveChangesAsync(CancellationToken.None)) + .Returns(Task.FromResult(1)); + + var unitOfWork = UnitOfWorkTestFixture.GetUnitOfWork(dbContextMock.Object, loggerMock.Object); + + var result = await unitOfWork.CommitAsync(); + + result.Should().BeTrue(); + } + + [Fact] + public async Task Should_Commit_Async_Returns_False() + { + var options = new DbContextOptionsBuilder(); + var dbContextMock = new Mock(options.Options); + var loggerMock = new Mock>>(); + + dbContextMock + .Setup(x => x.SaveChangesAsync(CancellationToken.None)) + .Throws(new DbUpdateException("Boom", new System.Exception("it broke"))); + + var unitOfWork = UnitOfWorkTestFixture.GetUnitOfWork(dbContextMock.Object, loggerMock.Object); + + var result = await unitOfWork.CommitAsync(); + + result.Should().BeFalse(); + } + + [Fact] + public async Task Should_Throw_Exception_When_Commiting_With_DbUpdateException() + { + var options = new DbContextOptionsBuilder(); + var dbContextMock = new Mock(options.Options); + var loggerMock = new Mock>>(); + + dbContextMock + .Setup(x => x.SaveChangesAsync(CancellationToken.None)) + .Throws(new Exception("boom")); + + var unitOfWork = UnitOfWorkTestFixture.GetUnitOfWork(dbContextMock.Object, loggerMock.Object); + + Func knalltAction = async () => await unitOfWork.CommitAsync(); + + await knalltAction.Should().ThrowAsync(); + } +} \ No newline at end of file diff --git a/CleanArchitecture.Infrastructure.Tests/UnitTest1.cs b/CleanArchitecture.Infrastructure.Tests/UnitTest1.cs deleted file mode 100644 index 3afe4ef..0000000 --- a/CleanArchitecture.Infrastructure.Tests/UnitTest1.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace CleanArchitecture.Infrastructure.Tests; - -public class UnitTest1 -{ - [Fact] - public void Test1() - { - } -} \ No newline at end of file diff --git a/CleanArchitecture.Infrastructure.Tests/Usings.cs b/CleanArchitecture.Infrastructure.Tests/Usings.cs deleted file mode 100644 index 8c927eb..0000000 --- a/CleanArchitecture.Infrastructure.Tests/Usings.cs +++ /dev/null @@ -1 +0,0 @@ -global using Xunit; \ No newline at end of file diff --git a/CleanArchitecture.Infrastructure/Database/ApplicationDbContext.cs b/CleanArchitecture.Infrastructure/Database/ApplicationDbContext.cs index 238da23..f4b4323 100644 --- a/CleanArchitecture.Infrastructure/Database/ApplicationDbContext.cs +++ b/CleanArchitecture.Infrastructure/Database/ApplicationDbContext.cs @@ -4,7 +4,7 @@ using Microsoft.EntityFrameworkCore; namespace CleanArchitecture.Infrastructure.Database; -public sealed class ApplicationDbContext : DbContext +public class ApplicationDbContext : DbContext { public DbSet Users { get; set; } = null!; diff --git a/CleanArchitecture.Infrastructure/Repositories/BaseRepository.cs b/CleanArchitecture.Infrastructure/Repositories/BaseRepository.cs index bde842f..f18e513 100644 --- a/CleanArchitecture.Infrastructure/Repositories/BaseRepository.cs +++ b/CleanArchitecture.Infrastructure/Repositories/BaseRepository.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using CleanArchitecture.Domain.Entities; -using CleanArchitecture.Domain.Interfaces; using CleanArchitecture.Domain.Interfaces.Repositories; using Microsoft.EntityFrameworkCore; @@ -73,4 +72,18 @@ public class BaseRepository : IRepository where TEntity : Enti { return _dbSet.AnyAsync(entity => entity.Id == id); } + + public void Remove(TEntity entity, bool hardDelete = false) + { + if (hardDelete) + { + _dbSet.Remove(entity); + } + else + { + entity.Delete(); + _dbSet.Update(entity); + } + } + } \ No newline at end of file diff --git a/Todo.txt b/Todo.txt index 028bfec..6681ff1 100644 --- a/Todo.txt +++ b/Todo.txt @@ -1,4 +1,6 @@ - Complete user endpoints - Add Tests - Remove warnings and apply suggestions +- Add Docker support +- Add gRPC support - Make repo public