0
0
mirror of https://github.com/alex289/CleanArchitecture.git synced 2025-06-30 02:31:08 +00:00

Add domain tests

This commit is contained in:
alex289 2023-03-08 22:40:09 +01:00
parent 305e6320f0
commit e937e786a7
No known key found for this signature in database
GPG Key ID: 573F77CD2D87F863
34 changed files with 942 additions and 17 deletions

View File

@ -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<IActionResult> CreateUserAsync([FromBody] CreateUserViewModel viewModel)
{
return "test";
await _userService.CreateUserAsync(viewModel);
return Response();
}
[HttpDelete("{id}")]
public string DeleteUserAsync([FromRoute] Guid id)
public async Task<IActionResult> DeleteUserAsync([FromRoute] Guid id)
{
return "test";
await _userService.DeleteUserAsync(id);
return Response(id);
}
[HttpPut]
public string UpdateUserAsync()
public async Task<IActionResult> UpdateUserAsync([FromBody] UpdateUserViewModel viewModel)
{
return "test";
await _userService.UpdateUserAsync(viewModel);
return Response(viewModel);
}
}

View File

@ -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);
}
}

View File

@ -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;

View File

@ -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<UserViewModel?> GetUserByUserIdAsync(Guid userId);
public Task<IEnumerable<UserViewModel>> GetAllUsersAsync();
public Task CreateUserAsync(CreateUserViewModel user);
public Task UpdateUserAsync(UpdateUserViewModel user);
public Task DeleteUserAsync(Guid userId);
}

View File

@ -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;

View File

@ -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;

View File

@ -1,5 +1,6 @@
using System;
using CleanArchitecture.Application.ViewModels;
using CleanArchitecture.Application.ViewModels.Users;
using MediatR;
namespace CleanArchitecture.Application.Queries.Users.GetUserById;

View File

@ -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;

View File

@ -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));
}
}

View File

@ -0,0 +1,6 @@
namespace CleanArchitecture.Application.ViewModels.Users;
public sealed record CreateUserViewModel(
string Email,
string Surname,
string GivenName);

View File

@ -0,0 +1,9 @@
using System;
namespace CleanArchitecture.Application.ViewModels.Users;
public sealed record UpdateUserViewModel(
Guid Id,
string Email,
string Surname,
string GivenName);

View File

@ -1,7 +1,7 @@
using System;
using CleanArchitecture.Domain.Entities;
namespace CleanArchitecture.Application.ViewModels;
namespace CleanArchitecture.Application.ViewModels.Users;
public sealed class UserViewModel
{

View File

@ -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<UserCreatedEvent>(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<UserCreatedEvent>()
.VerifyAnyDomainNotification()
.VerifyExistingNotification(
DomainErrorCodes.UserAlreadyExists,
$"There is already a User with Id {command.UserId}");
}
}

View File

@ -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<IUserRepository> UserRepository { get; }
public CreateUserCommandTestFixture()
{
UserRepository = new Mock<IUserRepository>();
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<Guid>(y => y == user.Id)))
.ReturnsAsync(user);
return user;
}
}

View File

@ -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<CreateUserCommand, CreateUserCommandValidation>
{
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");
}

View File

@ -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<UserDeletedEvent>(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<UserDeletedEvent>()
.VerifyAnyDomainNotification()
.VerifyExistingNotification(
ErrorCodes.ObjectNotFound,
$"There is no User with Id {command.UserId}");
}
}

View File

@ -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<IUserRepository> UserRepository { get; }
public DeleteUserCommandTestFixture()
{
UserRepository = new Mock<IUserRepository>();
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<Guid>(y => y == user.Id)))
.ReturnsAsync(user);
return user;
}
}

View File

@ -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<DeleteUserCommand, DeleteUserCommandValidation>
{
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());
}

View File

@ -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<UserUpdatedEvent>(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<UserUpdatedEvent>()
.VerifyAnyDomainNotification()
.VerifyExistingNotification(
ErrorCodes.ObjectNotFound,
$"There is no User with Id {command.UserId}");
}
}

View File

@ -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<IUserRepository> UserRepository { get; }
public UpdateUserCommandTestFixture()
{
UserRepository = new Mock<IUserRepository>();
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<Guid>(y => y == user.Id)))
.ReturnsAsync(user);
return user;
}
}

View File

@ -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<UpdateUserCommand, UpdateUserCommandValidation>
{
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");
}

View File

@ -10,9 +10,4 @@
<PackageReference Include="MediatR" Version="12.0.1" />
</ItemGroup>
<ItemGroup>
<Folder Include="Commands\Users\CreateUser" />
<Folder Include="Commands\Users\UpdateUser" />
</ItemGroup>
</Project>

View File

@ -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;
}
}

View File

@ -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<CreateUserCommand>
{
private readonly IUserRepository _userRepository;
public CreateUserCommandHandler(
IMediatorHandler bus,
IUnitOfWork unitOfWork,
INotificationHandler<DomainNotification> 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));
}
}
}

View File

@ -0,0 +1,56 @@
using CleanArchitecture.Domain.Errors;
using FluentValidation;
namespace CleanArchitecture.Domain.Commands.Users.CreateUser;
public sealed class CreateUserCommandValidation : AbstractValidator<CreateUserCommand>
{
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");
}
}

View File

@ -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));
}

View File

@ -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;
}
}

View File

@ -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<UpdateUserCommand>
{
private readonly IUserRepository _userRepository;
public UpdateUserCommandHandler(
IMediatorHandler bus,
IUnitOfWork unitOfWork,
INotificationHandler<DomainNotification> 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));
}
}
}

View File

@ -0,0 +1,56 @@
using CleanArchitecture.Domain.Errors;
using FluentValidation;
namespace CleanArchitecture.Domain.Commands.Users.UpdateUser;
public sealed class UpdateUserCommandValidation : AbstractValidator<UpdateUserCommand>
{
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");
}
}

View File

@ -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";
}

View File

@ -6,10 +6,22 @@ using MediatR;
namespace CleanArchitecture.Domain.EventHandler;
public sealed class UserEventHandler :
INotificationHandler<UserDeletedEvent>
INotificationHandler<UserDeletedEvent>,
INotificationHandler<UserCreatedEvent>,
INotificationHandler<UserUpdatedEvent>
{
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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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<IRequestHandler<CreateUserCommand>, CreateUserCommandHandler>();
services.AddScoped<IRequestHandler<UpdateUserCommand>, UpdateUserCommandHandler>();
services.AddScoped<IRequestHandler<DeleteUserCommand>, DeleteUserCommandHandler>();
return services;
@ -19,6 +23,8 @@ public static class ServiceCollectionExtension
public static IServiceCollection AddNotificationHandlers(this IServiceCollection services)
{
// User
services.AddScoped<INotificationHandler<UserCreatedEvent>, UserEventHandler>();
services.AddScoped<INotificationHandler<UserUpdatedEvent>, UserEventHandler>();
services.AddScoped<INotificationHandler<UserDeletedEvent>, UserEventHandler>();
return services;