From e937e786a7e4c63e80b745c8cfc1ddd3d09c1de0 Mon Sep 17 00:00:00 2001 From: alex289 Date: Wed, 8 Mar 2023 22:40:09 +0100 Subject: [PATCH] Add domain tests --- .../Controllers/UserController.cs | 16 ++- .../Users/GetAllUsersQueryHandlerTests.cs | 2 +- .../Extensions/ServiceCollectionExtension.cs | 1 + .../Interfaces/IUserService.cs | 5 +- .../Queries/Users/GetAll/GetAllUsersQuery.cs | 1 + .../Users/GetAll/GetAllUsersQueryHandler.cs | 1 + .../Users/GetUserById/GetUserByIdQuery.cs | 1 + .../GetUserById/GetUserByIdQueryHandler.cs | 1 + .../Services/UserService.cs | 28 +++- .../viewmodels/Users/CreateUserViewModel.cs | 6 + .../viewmodels/Users/UpdateUserViewModel.cs | 9 ++ .../viewmodels/{ => Users}/UserViewModel.cs | 2 +- .../CreateUserCommandHandlerTests.cs | 52 ++++++++ .../CreateUserCommandTestFixture.cs | 37 ++++++ .../CreateUserCommandValidationTests.cs | 120 ++++++++++++++++++ .../DeleteUserCommandHandlerTests.cs | 44 +++++++ .../DeleteUserCommandTestFixture.cs | 37 ++++++ .../DeleteUserCommandValidationTests.cs | 35 +++++ .../UpdateUserCommandHandlerTests.cs | 52 ++++++++ .../UpdateUserCommandTestFixture.cs | 37 ++++++ .../UpdateUserCommandValidationTests.cs | 120 ++++++++++++++++++ .../CleanArchitecture.Domain.csproj | 5 - .../Users/CreateUser/CreateUserCommand.cs | 31 +++++ .../CreateUser/CreateUserCommandHandler.cs | 59 +++++++++ .../CreateUser/CreateUserCommandValidation.cs | 56 ++++++++ .../DeleteUser/DeleteUserCommandHandler.cs | 2 +- .../Users/UpdateUser/UpdateUserCommand.cs | 31 +++++ .../UpdateUser/UpdateUserCommandHandler.cs | 56 ++++++++ .../UpdateUser/UpdateUserCommandValidation.cs | 56 ++++++++ .../Errors/DomainErrorCodes.cs | 10 ++ .../EventHandler/UserEventHandler.cs | 14 +- .../Events/User/UserCreatedEvent.cs | 13 ++ .../Events/User/UserUpdatedEvent.cs | 13 ++ .../Extensions/ServiceCollectionExtension.cs | 6 + 34 files changed, 942 insertions(+), 17 deletions(-) create mode 100644 CleanArchitecture.Application/viewmodels/Users/CreateUserViewModel.cs create mode 100644 CleanArchitecture.Application/viewmodels/Users/UpdateUserViewModel.cs rename CleanArchitecture.Application/viewmodels/{ => Users}/UserViewModel.cs (90%) create mode 100644 CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandHandlerTests.cs create mode 100644 CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandTestFixture.cs create mode 100644 CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandValidationTests.cs create mode 100644 CleanArchitecture.Domain.Tests/CommandHandler/User/DeleteUser/DeleteUserCommandHandlerTests.cs create mode 100644 CleanArchitecture.Domain.Tests/CommandHandler/User/DeleteUser/DeleteUserCommandTestFixture.cs create mode 100644 CleanArchitecture.Domain.Tests/CommandHandler/User/DeleteUser/DeleteUserCommandValidationTests.cs create mode 100644 CleanArchitecture.Domain.Tests/CommandHandler/User/UpdateUser/UpdateUserCommandHandlerTests.cs create mode 100644 CleanArchitecture.Domain.Tests/CommandHandler/User/UpdateUser/UpdateUserCommandTestFixture.cs create mode 100644 CleanArchitecture.Domain.Tests/CommandHandler/User/UpdateUser/UpdateUserCommandValidationTests.cs create mode 100644 CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommand.cs create mode 100644 CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommandHandler.cs create mode 100644 CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommandValidation.cs create mode 100644 CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommand.cs create mode 100644 CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommandHandler.cs create mode 100644 CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommandValidation.cs create mode 100644 CleanArchitecture.Domain/Events/User/UserCreatedEvent.cs create mode 100644 CleanArchitecture.Domain/Events/User/UserUpdatedEvent.cs diff --git a/CleanArchitecture.Api/Controllers/UserController.cs b/CleanArchitecture.Api/Controllers/UserController.cs index 791145d..3f28f6a 100644 --- a/CleanArchitecture.Api/Controllers/UserController.cs +++ b/CleanArchitecture.Api/Controllers/UserController.cs @@ -1,6 +1,7 @@ using System; using System.Threading.Tasks; using CleanArchitecture.Application.Interfaces; +using CleanArchitecture.Application.ViewModels.Users; using CleanArchitecture.Domain.Notifications; using MediatR; using Microsoft.AspNetCore.Mvc; @@ -35,20 +36,23 @@ public class UserController : ApiController } [HttpPost] - public string CreateUserAsync() + public async Task CreateUserAsync([FromBody] CreateUserViewModel viewModel) { - return "test"; + await _userService.CreateUserAsync(viewModel); + return Response(); } [HttpDelete("{id}")] - public string DeleteUserAsync([FromRoute] Guid id) + public async Task DeleteUserAsync([FromRoute] Guid id) { - return "test"; + await _userService.DeleteUserAsync(id); + return Response(id); } [HttpPut] - public string UpdateUserAsync() + public async Task UpdateUserAsync([FromBody] UpdateUserViewModel viewModel) { - return "test"; + await _userService.UpdateUserAsync(viewModel); + return Response(viewModel); } } \ No newline at end of file diff --git a/CleanArchitecture.Application.Tests/Queries/Users/GetAllUsersQueryHandlerTests.cs b/CleanArchitecture.Application.Tests/Queries/Users/GetAllUsersQueryHandlerTests.cs index 61519cb..18e44eb 100644 --- a/CleanArchitecture.Application.Tests/Queries/Users/GetAllUsersQueryHandlerTests.cs +++ b/CleanArchitecture.Application.Tests/Queries/Users/GetAllUsersQueryHandlerTests.cs @@ -19,6 +19,6 @@ public sealed class GetAllUsersQueryHandlerTests result.Should().NotBeNull(); result.Should().ContainSingle(); - result.FirstOrDefault().Id.Should().Be(_fixture.ExistingUserId); + result.FirstOrDefault()!.Id.Should().Be(_fixture.ExistingUserId); } } \ No newline at end of file diff --git a/CleanArchitecture.Application/Extensions/ServiceCollectionExtension.cs b/CleanArchitecture.Application/Extensions/ServiceCollectionExtension.cs index e4ffb13..752e40e 100644 --- a/CleanArchitecture.Application/Extensions/ServiceCollectionExtension.cs +++ b/CleanArchitecture.Application/Extensions/ServiceCollectionExtension.cs @@ -4,6 +4,7 @@ using CleanArchitecture.Application.Queries.Users.GetAll; using CleanArchitecture.Application.Queries.Users.GetUserById; using CleanArchitecture.Application.Services; using CleanArchitecture.Application.ViewModels; +using CleanArchitecture.Application.ViewModels.Users; using MediatR; using Microsoft.Extensions.DependencyInjection; diff --git a/CleanArchitecture.Application/Interfaces/IUserService.cs b/CleanArchitecture.Application/Interfaces/IUserService.cs index 243970a..ea7ad9c 100644 --- a/CleanArchitecture.Application/Interfaces/IUserService.cs +++ b/CleanArchitecture.Application/Interfaces/IUserService.cs @@ -1,7 +1,7 @@ -using CleanArchitecture.Application.ViewModels; using System.Threading.Tasks; using System; using System.Collections.Generic; +using CleanArchitecture.Application.ViewModels.Users; namespace CleanArchitecture.Application.Interfaces; @@ -9,4 +9,7 @@ public interface IUserService { public Task GetUserByUserIdAsync(Guid userId); public Task> GetAllUsersAsync(); + public Task CreateUserAsync(CreateUserViewModel user); + public Task UpdateUserAsync(UpdateUserViewModel user); + public Task DeleteUserAsync(Guid userId); } \ No newline at end of file diff --git a/CleanArchitecture.Application/Queries/Users/GetAll/GetAllUsersQuery.cs b/CleanArchitecture.Application/Queries/Users/GetAll/GetAllUsersQuery.cs index 8af4dda..76e942c 100644 --- a/CleanArchitecture.Application/Queries/Users/GetAll/GetAllUsersQuery.cs +++ b/CleanArchitecture.Application/Queries/Users/GetAll/GetAllUsersQuery.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using CleanArchitecture.Application.ViewModels; +using CleanArchitecture.Application.ViewModels.Users; using MediatR; namespace CleanArchitecture.Application.Queries.Users.GetAll; diff --git a/CleanArchitecture.Application/Queries/Users/GetAll/GetAllUsersQueryHandler.cs b/CleanArchitecture.Application/Queries/Users/GetAll/GetAllUsersQueryHandler.cs index d0a3b87..3ae90cd 100644 --- a/CleanArchitecture.Application/Queries/Users/GetAll/GetAllUsersQueryHandler.cs +++ b/CleanArchitecture.Application/Queries/Users/GetAll/GetAllUsersQueryHandler.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using CleanArchitecture.Application.ViewModels; +using CleanArchitecture.Application.ViewModels.Users; using CleanArchitecture.Domain.Interfaces.Repositories; using MediatR; using Microsoft.EntityFrameworkCore; diff --git a/CleanArchitecture.Application/Queries/Users/GetUserById/GetUserByIdQuery.cs b/CleanArchitecture.Application/Queries/Users/GetUserById/GetUserByIdQuery.cs index 0cf4a82..115a31a 100644 --- a/CleanArchitecture.Application/Queries/Users/GetUserById/GetUserByIdQuery.cs +++ b/CleanArchitecture.Application/Queries/Users/GetUserById/GetUserByIdQuery.cs @@ -1,5 +1,6 @@ using System; using CleanArchitecture.Application.ViewModels; +using CleanArchitecture.Application.ViewModels.Users; using MediatR; namespace CleanArchitecture.Application.Queries.Users.GetUserById; diff --git a/CleanArchitecture.Application/Queries/Users/GetUserById/GetUserByIdQueryHandler.cs b/CleanArchitecture.Application/Queries/Users/GetUserById/GetUserByIdQueryHandler.cs index dfcb036..af9a258 100644 --- a/CleanArchitecture.Application/Queries/Users/GetUserById/GetUserByIdQueryHandler.cs +++ b/CleanArchitecture.Application/Queries/Users/GetUserById/GetUserByIdQueryHandler.cs @@ -2,6 +2,7 @@ using System.Threading; using System.Threading.Tasks; using CleanArchitecture.Application.ViewModels; +using CleanArchitecture.Application.ViewModels.Users; using CleanArchitecture.Domain.Errors; using CleanArchitecture.Domain.Interfaces; using CleanArchitecture.Domain.Interfaces.Repositories; diff --git a/CleanArchitecture.Application/Services/UserService.cs b/CleanArchitecture.Application/Services/UserService.cs index 0b85778..a6e1a04 100644 --- a/CleanArchitecture.Application/Services/UserService.cs +++ b/CleanArchitecture.Application/Services/UserService.cs @@ -4,7 +4,10 @@ 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.Application.ViewModels.Users; +using CleanArchitecture.Domain.Commands.Users.CreateUser; +using CleanArchitecture.Domain.Commands.Users.DeleteUser; +using CleanArchitecture.Domain.Commands.Users.UpdateUser; using CleanArchitecture.Domain.Interfaces; namespace CleanArchitecture.Application.Services; @@ -27,4 +30,27 @@ public sealed class UserService : IUserService { return await _bus.QueryAsync(new GetAllUsersQuery()); } + + public async Task CreateUserAsync(CreateUserViewModel user) + { + await _bus.SendCommandAsync(new CreateUserCommand( + Guid.NewGuid(), + user.Email, + user.Surname, + user.GivenName)); + } + + public async Task UpdateUserAsync(UpdateUserViewModel user) + { + await _bus.SendCommandAsync(new UpdateUserCommand( + user.Id, + user.Email, + user.Surname, + user.GivenName)); + } + + public async Task DeleteUserAsync(Guid userId) + { + await _bus.SendCommandAsync(new DeleteUserCommand(userId)); + } } \ No newline at end of file diff --git a/CleanArchitecture.Application/viewmodels/Users/CreateUserViewModel.cs b/CleanArchitecture.Application/viewmodels/Users/CreateUserViewModel.cs new file mode 100644 index 0000000..c1a01d4 --- /dev/null +++ b/CleanArchitecture.Application/viewmodels/Users/CreateUserViewModel.cs @@ -0,0 +1,6 @@ +namespace CleanArchitecture.Application.ViewModels.Users; + +public sealed record CreateUserViewModel( + string Email, + string Surname, + string GivenName); \ No newline at end of file diff --git a/CleanArchitecture.Application/viewmodels/Users/UpdateUserViewModel.cs b/CleanArchitecture.Application/viewmodels/Users/UpdateUserViewModel.cs new file mode 100644 index 0000000..e64fd7d --- /dev/null +++ b/CleanArchitecture.Application/viewmodels/Users/UpdateUserViewModel.cs @@ -0,0 +1,9 @@ +using System; + +namespace CleanArchitecture.Application.ViewModels.Users; + +public sealed record UpdateUserViewModel( + Guid Id, + string Email, + string Surname, + string GivenName); \ No newline at end of file diff --git a/CleanArchitecture.Application/viewmodels/UserViewModel.cs b/CleanArchitecture.Application/viewmodels/Users/UserViewModel.cs similarity index 90% rename from CleanArchitecture.Application/viewmodels/UserViewModel.cs rename to CleanArchitecture.Application/viewmodels/Users/UserViewModel.cs index f59a955..9ecc82f 100644 --- a/CleanArchitecture.Application/viewmodels/UserViewModel.cs +++ b/CleanArchitecture.Application/viewmodels/Users/UserViewModel.cs @@ -1,7 +1,7 @@ using System; using CleanArchitecture.Domain.Entities; -namespace CleanArchitecture.Application.ViewModels; +namespace CleanArchitecture.Application.ViewModels.Users; public sealed class UserViewModel { diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandHandlerTests.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandHandlerTests.cs new file mode 100644 index 0000000..6cf2b53 --- /dev/null +++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandHandlerTests.cs @@ -0,0 +1,52 @@ +using CleanArchitecture.Domain.Commands.Users.CreateUser; +using CleanArchitecture.Domain.Errors; +using CleanArchitecture.Domain.Events.User; +using Xunit; + +namespace CleanArchitecture.Domain.Tests.CommandHandler.User.CreateUser; + +public sealed class CreateUserCommandHandlerTests +{ + private readonly CreateUserCommandTestFixture _fixture = new(); + + [Fact] + public void Should_Create_User() + { + _fixture.SetupUser(); + + var command = new CreateUserCommand( + Guid.NewGuid(), + "test@email.com", + "Test", + "Email"); + + _fixture.CommandHandler.Handle(command, default).Wait(); + + _fixture + .VerifyNoDomainNotification() + .VerifyCommit() + .VerifyRaisedEvent(x => x.UserId == command.UserId); + } + + [Fact] + public void Should_Not_Create_Already_Existing_User() + { + var user = _fixture.SetupUser(); + + var command = new CreateUserCommand( + user.Id, + "test@email.com", + "Test", + "Email"); + + _fixture.CommandHandler.Handle(command, default).Wait(); + + _fixture + .VerifyNoCommit() + .VerifyNoRaisedEvent() + .VerifyAnyDomainNotification() + .VerifyExistingNotification( + DomainErrorCodes.UserAlreadyExists, + $"There is already a User with Id {command.UserId}"); + } +} \ No newline at end of file diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandTestFixture.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandTestFixture.cs new file mode 100644 index 0000000..50a82da --- /dev/null +++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandTestFixture.cs @@ -0,0 +1,37 @@ +using CleanArchitecture.Domain.Commands.Users.CreateUser; +using CleanArchitecture.Domain.Interfaces.Repositories; +using Moq; + +namespace CleanArchitecture.Domain.Tests.CommandHandler.User.CreateUser; + +public sealed class CreateUserCommandTestFixture : CommandHandlerFixtureBase +{ + public CreateUserCommandHandler CommandHandler { get; } + private Mock UserRepository { get; } + + public CreateUserCommandTestFixture() + { + UserRepository = new Mock(); + + CommandHandler = new( + Bus.Object, + UnitOfWork.Object, + NotificationHandler.Object, + UserRepository.Object); + } + + public Entities.User SetupUser() + { + var user = new Entities.User( + Guid.NewGuid(), + "max@mustermann.com", + "Max", + "Mustermann"); + + UserRepository + .Setup(x => x.GetByIdAsync(It.Is(y => y == user.Id))) + .ReturnsAsync(user); + + return user; + } +} \ No newline at end of file diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandValidationTests.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandValidationTests.cs new file mode 100644 index 0000000..60fd654 --- /dev/null +++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandValidationTests.cs @@ -0,0 +1,120 @@ +using CleanArchitecture.Domain.Commands.Users.CreateUser; +using CleanArchitecture.Domain.Errors; +using Xunit; + +namespace CleanArchitecture.Domain.Tests.CommandHandler.User.CreateUser; + +public sealed class CreateUserCommandValidationTests : + ValidationTestBase +{ + public CreateUserCommandValidationTests() : base(new CreateUserCommandValidation()) + { + } + + [Fact] + public void Should_Be_Valid() + { + var command = CreateTestCommand(); + + ShouldBeValid(command); + } + + [Fact] + public void Should_Be_Invalid_For_Empty_User_Id() + { + var command = CreateTestCommand(userId: Guid.Empty); + + ShouldHaveSingleError( + command, + DomainErrorCodes.UserEmptyId, + "User id may not be empty"); + } + + [Fact] + public void Should_Be_Invalid_For_Empty_Email() + { + var command = CreateTestCommand(email: string.Empty); + + ShouldHaveSingleError( + command, + DomainErrorCodes.UserInvalidEmail, + "Email is not a valid email address"); + } + + [Fact] + public void Should_Be_Invalid_For_Invalid_Email() + { + var command = CreateTestCommand(email: "not a email"); + + ShouldHaveSingleError( + command, + DomainErrorCodes.UserInvalidEmail, + "Email is not a valid email address"); + } + + [Fact] + public void Should_Be_Invalid_For_Email_Exceeds_Max_Length() + { + var command = CreateTestCommand(email: new string('a', 320) + "@test.com"); + + ShouldHaveSingleError( + command, + DomainErrorCodes.UserEmailExceedsMaxLength, + "Email may not be longer than 320 characters"); + } + + [Fact] + public void Should_Be_Invalid_For_Empty_Surname() + { + var command = CreateTestCommand(surName: ""); + + ShouldHaveSingleError( + command, + DomainErrorCodes.UserEmptySurname, + "Surname may not be empty"); + } + + [Fact] + public void Should_Be_Invalid_For_Surname_Exceeds_Max_Length() + { + var command = CreateTestCommand(surName: new string('a', 101)); + + ShouldHaveSingleError( + command, + DomainErrorCodes.UserSurnameExceedsMaxLength, + "Surname may not be longer than 100 characters"); + } + + [Fact] + public void Should_Be_Invalid_For_Empty_Given_Name() + { + var command = CreateTestCommand(givenName: ""); + + ShouldHaveSingleError( + command, + DomainErrorCodes.UserEmptyGivenName, + "Given name may not be empty"); + } + + [Fact] + public void Should_Be_Invalid_For_Given_Name_Exceeds_Max_Length() + { + var command = CreateTestCommand(givenName: new string('a', 101)); + + ShouldHaveSingleError( + command, + DomainErrorCodes.UserGivenNameExceedsMaxLength, + "Given name may not be longer than 100 characters"); + } + + private CreateUserCommand CreateTestCommand( + Guid? userId = null, + string? email = null, + string? surName = null, + string? givenName = null) => + new ( + userId ?? Guid.NewGuid(), + email ?? "test@email.com", + surName ?? "test", + givenName ?? "email"); +} \ No newline at end of file diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/User/DeleteUser/DeleteUserCommandHandlerTests.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/DeleteUser/DeleteUserCommandHandlerTests.cs new file mode 100644 index 0000000..ef70639 --- /dev/null +++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/DeleteUser/DeleteUserCommandHandlerTests.cs @@ -0,0 +1,44 @@ +using CleanArchitecture.Domain.Commands.Users.DeleteUser; +using CleanArchitecture.Domain.Errors; +using CleanArchitecture.Domain.Events.User; +using Xunit; + +namespace CleanArchitecture.Domain.Tests.CommandHandler.User.DeleteUser; + +public sealed class DeleteUserCommandHandlerTests +{ + private readonly DeleteUserCommandTestFixture _fixture = new(); + + [Fact] + public void Should_Delete_User() + { + var user = _fixture.SetupUser(); + + var command = new DeleteUserCommand(user.Id); + + _fixture.CommandHandler.Handle(command, default).Wait(); + + _fixture + .VerifyNoDomainNotification() + .VerifyCommit() + .VerifyRaisedEvent(x => x.UserId == user.Id); + } + + [Fact] + public void Should_Not_Delete_Non_Existing_User() + { + _fixture.SetupUser(); + + var command = new DeleteUserCommand(Guid.NewGuid()); + + _fixture.CommandHandler.Handle(command, default).Wait(); + + _fixture + .VerifyNoCommit() + .VerifyNoRaisedEvent() + .VerifyAnyDomainNotification() + .VerifyExistingNotification( + ErrorCodes.ObjectNotFound, + $"There is no User with Id {command.UserId}"); + } +} \ No newline at end of file diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/User/DeleteUser/DeleteUserCommandTestFixture.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/DeleteUser/DeleteUserCommandTestFixture.cs new file mode 100644 index 0000000..9da0eee --- /dev/null +++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/DeleteUser/DeleteUserCommandTestFixture.cs @@ -0,0 +1,37 @@ +using CleanArchitecture.Domain.Commands.Users.DeleteUser; +using CleanArchitecture.Domain.Interfaces.Repositories; +using Moq; + +namespace CleanArchitecture.Domain.Tests.CommandHandler.User.DeleteUser; + +public sealed class DeleteUserCommandTestFixture : CommandHandlerFixtureBase +{ + public DeleteUserCommandHandler CommandHandler { get; } + private Mock UserRepository { get; } + + public DeleteUserCommandTestFixture() + { + UserRepository = new Mock(); + + CommandHandler = new ( + Bus.Object, + UnitOfWork.Object, + NotificationHandler.Object, + UserRepository.Object); + } + + public Entities.User SetupUser() + { + var user = new Entities.User( + Guid.NewGuid(), + "max@mustermann.com", + "Max", + "Mustermann"); + + UserRepository + .Setup(x => x.GetByIdAsync(It.Is(y => y == user.Id))) + .ReturnsAsync(user); + + return user; + } +} \ No newline at end of file diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/User/DeleteUser/DeleteUserCommandValidationTests.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/DeleteUser/DeleteUserCommandValidationTests.cs new file mode 100644 index 0000000..2c70984 --- /dev/null +++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/DeleteUser/DeleteUserCommandValidationTests.cs @@ -0,0 +1,35 @@ +using CleanArchitecture.Domain.Commands.Users.DeleteUser; +using CleanArchitecture.Domain.Errors; +using Xunit; + +namespace CleanArchitecture.Domain.Tests.CommandHandler.User.DeleteUser; + +public sealed class DeleteUserCommandValidationTests : + ValidationTestBase +{ + public DeleteUserCommandValidationTests() : base(new DeleteUserCommandValidation()) + { + } + + [Fact] + public void Should_Be_Valid() + { + var command = CreateTestCommand(); + + ShouldBeValid(command); + } + + [Fact] + public void Should_Be_Invalid_For_Empty_User_Id() + { + var command = CreateTestCommand(userId: Guid.Empty); + + ShouldHaveSingleError( + command, + DomainErrorCodes.UserEmptyId, + "User id may not be empty"); + } + + private DeleteUserCommand CreateTestCommand(Guid? userId = null) => + new (userId ?? Guid.NewGuid()); +} \ No newline at end of file diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/User/UpdateUser/UpdateUserCommandHandlerTests.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/UpdateUser/UpdateUserCommandHandlerTests.cs new file mode 100644 index 0000000..182f9ed --- /dev/null +++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/UpdateUser/UpdateUserCommandHandlerTests.cs @@ -0,0 +1,52 @@ +using CleanArchitecture.Domain.Commands.Users.UpdateUser; +using CleanArchitecture.Domain.Errors; +using CleanArchitecture.Domain.Events.User; +using Xunit; + +namespace CleanArchitecture.Domain.Tests.CommandHandler.User.UpdateUser; + +public sealed class UpdateUserCommandHandlerTests +{ + private readonly UpdateUserCommandTestFixture _fixture = new(); + + [Fact] + public async Task Should_Update_User() + { + var user = _fixture.SetupUser(); + + var command = new UpdateUserCommand( + user.Id, + "test@email.com", + "Test", + "Email"); + + await _fixture.CommandHandler.Handle(command, default); + + _fixture + .VerifyNoDomainNotification() + .VerifyCommit() + .VerifyRaisedEvent(x => x.UserId == command.UserId); + } + + [Fact] + public async Task Should_Not_Update_Non_Existing_User() + { + var user = _fixture.SetupUser(); + + var command = new UpdateUserCommand( + Guid.NewGuid(), + "test@email.com", + "Test", + "Email"); + + await _fixture.CommandHandler.Handle(command, default); + + _fixture + .VerifyNoCommit() + .VerifyNoRaisedEvent() + .VerifyAnyDomainNotification() + .VerifyExistingNotification( + ErrorCodes.ObjectNotFound, + $"There is no User with Id {command.UserId}"); + } +} \ No newline at end of file diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/User/UpdateUser/UpdateUserCommandTestFixture.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/UpdateUser/UpdateUserCommandTestFixture.cs new file mode 100644 index 0000000..d3dd415 --- /dev/null +++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/UpdateUser/UpdateUserCommandTestFixture.cs @@ -0,0 +1,37 @@ +using CleanArchitecture.Domain.Commands.Users.UpdateUser; +using CleanArchitecture.Domain.Interfaces.Repositories; +using Moq; + +namespace CleanArchitecture.Domain.Tests.CommandHandler.User.UpdateUser; + +public sealed class UpdateUserCommandTestFixture : CommandHandlerFixtureBase +{ + public UpdateUserCommandHandler CommandHandler { get; } + private Mock UserRepository { get; } + + public UpdateUserCommandTestFixture() + { + UserRepository = new Mock(); + + CommandHandler = new( + Bus.Object, + UnitOfWork.Object, + NotificationHandler.Object, + UserRepository.Object); + } + + public Entities.User SetupUser() + { + var user = new Entities.User( + Guid.NewGuid(), + "max@mustermann.com", + "Max", + "Mustermann"); + + UserRepository + .Setup(x => x.GetByIdAsync(It.Is(y => y == user.Id))) + .ReturnsAsync(user); + + return user; + } +} \ No newline at end of file diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/User/UpdateUser/UpdateUserCommandValidationTests.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/UpdateUser/UpdateUserCommandValidationTests.cs new file mode 100644 index 0000000..9b06cac --- /dev/null +++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/UpdateUser/UpdateUserCommandValidationTests.cs @@ -0,0 +1,120 @@ +using CleanArchitecture.Domain.Commands.Users.UpdateUser; +using CleanArchitecture.Domain.Errors; +using Xunit; + +namespace CleanArchitecture.Domain.Tests.CommandHandler.User.UpdateUser; + +public sealed class UpdateUserCommandValidationTests : + ValidationTestBase +{ + public UpdateUserCommandValidationTests() : base(new UpdateUserCommandValidation()) + { + } + + [Fact] + public void Should_Be_Valid() + { + var command = CreateTestCommand(); + + ShouldBeValid(command); + } + + [Fact] + public void Should_Be_Invalid_For_Empty_User_Id() + { + var command = CreateTestCommand(userId: Guid.Empty); + + ShouldHaveSingleError( + command, + DomainErrorCodes.UserEmptyId, + "User id may not be empty"); + } + + [Fact] + public void Should_Be_Invalid_For_Empty_Email() + { + var command = CreateTestCommand(email: string.Empty); + + ShouldHaveSingleError( + command, + DomainErrorCodes.UserInvalidEmail, + "Email is not a valid email address"); + } + + [Fact] + public void Should_Be_Invalid_For_Invalid_Email() + { + var command = CreateTestCommand(email: "not a email"); + + ShouldHaveSingleError( + command, + DomainErrorCodes.UserInvalidEmail, + "Email is not a valid email address"); + } + + [Fact] + public void Should_Be_Invalid_For_Email_Exceeds_Max_Length() + { + var command = CreateTestCommand(email: new string('a', 320) + "@test.com"); + + ShouldHaveSingleError( + command, + DomainErrorCodes.UserEmailExceedsMaxLength, + "Email may not be longer than 320 characters"); + } + + [Fact] + public void Should_Be_Invalid_For_Empty_Surname() + { + var command = CreateTestCommand(surName: ""); + + ShouldHaveSingleError( + command, + DomainErrorCodes.UserEmptySurname, + "Surname may not be empty"); + } + + [Fact] + public void Should_Be_Invalid_For_Surname_Exceeds_Max_Length() + { + var command = CreateTestCommand(surName: new string('a', 101)); + + ShouldHaveSingleError( + command, + DomainErrorCodes.UserSurnameExceedsMaxLength, + "Surname may not be longer than 100 characters"); + } + + [Fact] + public void Should_Be_Invalid_For_Empty_Given_Name() + { + var command = CreateTestCommand(givenName: ""); + + ShouldHaveSingleError( + command, + DomainErrorCodes.UserEmptyGivenName, + "Given name may not be empty"); + } + + [Fact] + public void Should_Be_Invalid_For_Given_Name_Exceeds_Max_Length() + { + var command = CreateTestCommand(givenName: new string('a', 101)); + + ShouldHaveSingleError( + command, + DomainErrorCodes.UserGivenNameExceedsMaxLength, + "Given name may not be longer than 100 characters"); + } + + private UpdateUserCommand CreateTestCommand( + Guid? userId = null, + string? email = null, + string? surName = null, + string? givenName = null) => + new ( + userId ?? Guid.NewGuid(), + email ?? "test@email.com", + surName ?? "test", + givenName ?? "email"); +} \ No newline at end of file diff --git a/CleanArchitecture.Domain/CleanArchitecture.Domain.csproj b/CleanArchitecture.Domain/CleanArchitecture.Domain.csproj index 847601c..8c0626a 100644 --- a/CleanArchitecture.Domain/CleanArchitecture.Domain.csproj +++ b/CleanArchitecture.Domain/CleanArchitecture.Domain.csproj @@ -10,9 +10,4 @@ - - - - - diff --git a/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommand.cs b/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommand.cs new file mode 100644 index 0000000..3762540 --- /dev/null +++ b/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommand.cs @@ -0,0 +1,31 @@ +using System; + +namespace CleanArchitecture.Domain.Commands.Users.CreateUser; + +public sealed class CreateUserCommand : CommandBase +{ + private static readonly CreateUserCommandValidation _validation = new(); + + public Guid UserId { get; } + public string Email { get; } + public string Surname { get; } + public string GivenName { get; } + + public CreateUserCommand( + Guid userId, + string email, + string surname, + string givenName) : base(userId) + { + UserId = userId; + Email = email; + Surname = surname; + GivenName = givenName; + } + + public override bool IsValid() + { + ValidationResult = _validation.Validate(this); + return ValidationResult.IsValid; + } +} \ No newline at end of file diff --git a/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommandHandler.cs b/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommandHandler.cs new file mode 100644 index 0000000..0ebbd00 --- /dev/null +++ b/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommandHandler.cs @@ -0,0 +1,59 @@ +using System.Threading; +using System.Threading.Tasks; +using CleanArchitecture.Domain.Entities; +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.CreateUser; + +public sealed class CreateUserCommandHandler : CommandHandlerBase, + IRequestHandler +{ + private readonly IUserRepository _userRepository; + + public CreateUserCommandHandler( + IMediatorHandler bus, + IUnitOfWork unitOfWork, + INotificationHandler notifications, + IUserRepository userRepository) : base(bus, unitOfWork, notifications) + { + _userRepository = userRepository; + } + + public async Task Handle(CreateUserCommand request, CancellationToken cancellationToken) + { + if (!await TestValidityAsync(request)) + { + return; + } + + var existingUser = await _userRepository.GetByIdAsync(request.UserId); + + if (existingUser != null) + { + await _bus.RaiseEventAsync( + new DomainNotification( + request.MessageType, + $"There is already a User with Id {request.UserId}", + DomainErrorCodes.UserAlreadyExists)); + return; + } + + var user = new User( + request.UserId, + request.Email, + request.Surname, + request.GivenName); + + _userRepository.Add(user); + + if (await CommitAsync()) + { + await _bus.RaiseEventAsync(new UserCreatedEvent(user.Id)); + } + } +} \ No newline at end of file diff --git a/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommandValidation.cs b/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommandValidation.cs new file mode 100644 index 0000000..3c14aef --- /dev/null +++ b/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommandValidation.cs @@ -0,0 +1,56 @@ +using CleanArchitecture.Domain.Errors; +using FluentValidation; + +namespace CleanArchitecture.Domain.Commands.Users.CreateUser; + +public sealed class CreateUserCommandValidation : AbstractValidator +{ + public CreateUserCommandValidation() + { + AddRuleForId(); + AddRuleForEmail(); + AddRuleForSurname(); + AddRuleForGivenName(); + } + + private void AddRuleForId() + { + RuleFor(cmd => cmd.UserId) + .NotEmpty() + .WithErrorCode(DomainErrorCodes.UserEmptyId) + .WithMessage("User id may not be empty"); + } + + private void AddRuleForEmail() + { + RuleFor(cmd => cmd.Email) + .EmailAddress() + .WithErrorCode(DomainErrorCodes.UserInvalidEmail) + .WithMessage("Email is not a valid email address") + .MaximumLength(320) + .WithErrorCode(DomainErrorCodes.UserEmailExceedsMaxLength) + .WithMessage("Email may not be longer than 320 characters"); + } + + private void AddRuleForSurname() + { + RuleFor(cmd => cmd.Surname) + .NotEmpty() + .WithErrorCode(DomainErrorCodes.UserEmptySurname) + .WithMessage("Surname may not be empty") + .MaximumLength(100) + .WithErrorCode(DomainErrorCodes.UserSurnameExceedsMaxLength) + .WithMessage("Surname may not be longer than 100 characters"); + } + + private void AddRuleForGivenName() + { + RuleFor(cmd => cmd.GivenName) + .NotEmpty() + .WithErrorCode(DomainErrorCodes.UserEmptyGivenName) + .WithMessage("Given name may not be empty") + .MaximumLength(100) + .WithErrorCode(DomainErrorCodes.UserGivenNameExceedsMaxLength) + .WithMessage("Given name may not be longer than 100 characters"); + } +} \ No newline at end of file diff --git a/CleanArchitecture.Domain/Commands/Users/DeleteUser/DeleteUserCommandHandler.cs b/CleanArchitecture.Domain/Commands/Users/DeleteUser/DeleteUserCommandHandler.cs index b7abb33..edf9b0c 100644 --- a/CleanArchitecture.Domain/Commands/Users/DeleteUser/DeleteUserCommandHandler.cs +++ b/CleanArchitecture.Domain/Commands/Users/DeleteUser/DeleteUserCommandHandler.cs @@ -45,7 +45,7 @@ public sealed class DeleteUserCommandHandler : CommandHandlerBase, _userRepository.Remove(user); - if (!await CommitAsync()) + if (await CommitAsync()) { await _bus.RaiseEventAsync(new UserDeletedEvent(request.UserId)); } diff --git a/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommand.cs b/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommand.cs new file mode 100644 index 0000000..44e3617 --- /dev/null +++ b/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommand.cs @@ -0,0 +1,31 @@ +using System; + +namespace CleanArchitecture.Domain.Commands.Users.UpdateUser; + +public sealed class UpdateUserCommand : CommandBase +{ + private readonly UpdateUserCommandValidation _validation = new(); + + public Guid UserId { get; } + public string Email { get; } + public string Surname { get; } + public string GivenName { get; } + + public UpdateUserCommand( + Guid userId, + string email, + string surname, + string givenName) : base(userId) + { + UserId = userId; + Email = email; + Surname = surname; + GivenName = givenName; + } + + public override bool IsValid() + { + ValidationResult = _validation.Validate(this); + return ValidationResult.IsValid; + } +} \ No newline at end of file diff --git a/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommandHandler.cs b/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommandHandler.cs new file mode 100644 index 0000000..a94c5b5 --- /dev/null +++ b/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommandHandler.cs @@ -0,0 +1,56 @@ +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.UpdateUser; + +public sealed class UpdateUserCommandHandler : CommandHandlerBase, + IRequestHandler +{ + private readonly IUserRepository _userRepository; + + public UpdateUserCommandHandler( + IMediatorHandler bus, + IUnitOfWork unitOfWork, + INotificationHandler notifications, + IUserRepository userRepository) : base(bus, unitOfWork, notifications) + { + _userRepository = userRepository; + } + + public async Task Handle(UpdateUserCommand request, CancellationToken cancellationToken) + { + if (!await TestValidityAsync(request)) + { + return; + } + + var user = await _userRepository.GetByIdAsync(request.UserId); + + if (user == null) + { + await _bus.RaiseEventAsync( + new DomainNotification( + request.MessageType, + $"There is no User with Id {request.UserId}", + ErrorCodes.ObjectNotFound)); + return; + } + + user.SetEmail(request.Email); + user.SetSurname(request.Surname); + user.SetGivenName(request.GivenName); + + _userRepository.Update(user); + + if (await CommitAsync()) + { + await _bus.RaiseEventAsync(new UserUpdatedEvent(user.Id)); + } + } +} \ No newline at end of file diff --git a/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommandValidation.cs b/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommandValidation.cs new file mode 100644 index 0000000..0b7cc35 --- /dev/null +++ b/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommandValidation.cs @@ -0,0 +1,56 @@ +using CleanArchitecture.Domain.Errors; +using FluentValidation; + +namespace CleanArchitecture.Domain.Commands.Users.UpdateUser; + +public sealed class UpdateUserCommandValidation : AbstractValidator +{ + public UpdateUserCommandValidation() + { + AddRuleForId(); + AddRuleForEmail(); + AddRuleForSurname(); + AddRuleForGivenName(); + } + + private void AddRuleForId() + { + RuleFor(cmd => cmd.UserId) + .NotEmpty() + .WithErrorCode(DomainErrorCodes.UserEmptyId) + .WithMessage("User id may not be empty"); + } + + private void AddRuleForEmail() + { + RuleFor(cmd => cmd.Email) + .EmailAddress() + .WithErrorCode(DomainErrorCodes.UserInvalidEmail) + .WithMessage("Email is not a valid email address") + .MaximumLength(320) + .WithErrorCode(DomainErrorCodes.UserEmailExceedsMaxLength) + .WithMessage("Email may not be longer than 320 characters"); + } + + private void AddRuleForSurname() + { + RuleFor(cmd => cmd.Surname) + .NotEmpty() + .WithErrorCode(DomainErrorCodes.UserEmptySurname) + .WithMessage("Surname may not be empty") + .MaximumLength(100) + .WithErrorCode(DomainErrorCodes.UserSurnameExceedsMaxLength) + .WithMessage("Surname may not be longer than 100 characters"); + } + + private void AddRuleForGivenName() + { + RuleFor(cmd => cmd.GivenName) + .NotEmpty() + .WithErrorCode(DomainErrorCodes.UserEmptyGivenName) + .WithMessage("Given name may not be empty") + .MaximumLength(100) + .WithErrorCode(DomainErrorCodes.UserGivenNameExceedsMaxLength) + .WithMessage("Given name may not be longer than 100 characters"); + } +} \ No newline at end of file diff --git a/CleanArchitecture.Domain/Errors/DomainErrorCodes.cs b/CleanArchitecture.Domain/Errors/DomainErrorCodes.cs index 84d97c8..045b455 100644 --- a/CleanArchitecture.Domain/Errors/DomainErrorCodes.cs +++ b/CleanArchitecture.Domain/Errors/DomainErrorCodes.cs @@ -2,5 +2,15 @@ namespace CleanArchitecture.Domain.Errors; public static class DomainErrorCodes { + // User Validation public const string UserEmptyId = "USER_EMPTY_ID"; + public const string UserEmptySurname = "USER_EMPTY_SURNAME"; + public const string UserEmptyGivenName = "USER_EMPTY_GIVEN_NAME"; + public const string UserEmailExceedsMaxLength = "USER_EMAIL_EXCEEDS_MAX_LENGTH"; + public const string UserSurnameExceedsMaxLength = "USER_SURNAME_EXCEEDS_MAX_LENGTH"; + public const string UserGivenNameExceedsMaxLength = "USER_GIVEN_NAME_EXCEEDS_MAX_LENGTH"; + public const string UserInvalidEmail = "USER_INVALID_EMAIL"; + + // User + public const string UserAlreadyExists = "USER_ALREADY_EXISTS"; } \ No newline at end of file diff --git a/CleanArchitecture.Domain/EventHandler/UserEventHandler.cs b/CleanArchitecture.Domain/EventHandler/UserEventHandler.cs index b592693..45428b8 100644 --- a/CleanArchitecture.Domain/EventHandler/UserEventHandler.cs +++ b/CleanArchitecture.Domain/EventHandler/UserEventHandler.cs @@ -6,10 +6,22 @@ using MediatR; namespace CleanArchitecture.Domain.EventHandler; public sealed class UserEventHandler : - INotificationHandler + INotificationHandler, + INotificationHandler, + INotificationHandler { public Task Handle(UserDeletedEvent notification, CancellationToken cancellationToken) { return Task.CompletedTask; } + + public Task Handle(UserCreatedEvent notification, CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + + public Task Handle(UserUpdatedEvent notification, CancellationToken cancellationToken) + { + return Task.CompletedTask; + } } \ No newline at end of file diff --git a/CleanArchitecture.Domain/Events/User/UserCreatedEvent.cs b/CleanArchitecture.Domain/Events/User/UserCreatedEvent.cs new file mode 100644 index 0000000..19f3658 --- /dev/null +++ b/CleanArchitecture.Domain/Events/User/UserCreatedEvent.cs @@ -0,0 +1,13 @@ +using System; + +namespace CleanArchitecture.Domain.Events.User; + +public sealed class UserCreatedEvent : DomainEvent +{ + public Guid UserId { get; } + + public UserCreatedEvent(Guid userId) : base(userId) + { + UserId = userId; + } +} \ No newline at end of file diff --git a/CleanArchitecture.Domain/Events/User/UserUpdatedEvent.cs b/CleanArchitecture.Domain/Events/User/UserUpdatedEvent.cs new file mode 100644 index 0000000..996c7e6 --- /dev/null +++ b/CleanArchitecture.Domain/Events/User/UserUpdatedEvent.cs @@ -0,0 +1,13 @@ +using System; + +namespace CleanArchitecture.Domain.Events.User; + +public sealed class UserUpdatedEvent : DomainEvent +{ + public Guid UserId { get; } + + public UserUpdatedEvent(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 index 26cd510..64cd05d 100644 --- a/CleanArchitecture.Domain/Extensions/ServiceCollectionExtension.cs +++ b/CleanArchitecture.Domain/Extensions/ServiceCollectionExtension.cs @@ -1,4 +1,6 @@ +using CleanArchitecture.Domain.Commands.Users.CreateUser; using CleanArchitecture.Domain.Commands.Users.DeleteUser; +using CleanArchitecture.Domain.Commands.Users.UpdateUser; using CleanArchitecture.Domain.EventHandler; using CleanArchitecture.Domain.Events.User; using MediatR; @@ -11,6 +13,8 @@ public static class ServiceCollectionExtension public static IServiceCollection AddCommandHandlers(this IServiceCollection services) { // User + services.AddScoped, CreateUserCommandHandler>(); + services.AddScoped, UpdateUserCommandHandler>(); services.AddScoped, DeleteUserCommandHandler>(); return services; @@ -19,6 +23,8 @@ public static class ServiceCollectionExtension public static IServiceCollection AddNotificationHandlers(this IServiceCollection services) { // User + services.AddScoped, UserEventHandler>(); + services.AddScoped, UserEventHandler>(); services.AddScoped, UserEventHandler>(); return services;