0
0
mirror of https://github.com/alex289/CleanArchitecture.git synced 2025-06-29 18:21:08 +00:00

Add first command and initial tests

This commit is contained in:
alex289 2023-03-06 23:20:16 +01:00
parent cf71754fb0
commit 7b31d2dd1b
No known key found for this signature in database
GPG Key ID: 573F77CD2D87F863
38 changed files with 564 additions and 39 deletions

View File

@ -21,9 +21,10 @@ public class UserController : ApiController
}
[HttpGet]
public string GetAllUsersAsync()
public async Task<IActionResult> GetAllUsersAsync()
{
return "test";
var users = await _userService.GetAllUsersAsync();
return Response(users);
}
[HttpGet("{id}")]

View File

@ -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<ApplicationDbContext>(options =>
builder.Services.AddInfrastructure();
builder.Services.AddQueryHandlers();
builder.Services.AddServices();
builder.Services.AddCommandHandlers();
builder.Services.AddNotificationHandlers();
builder.Services.AddMediatR(cfg =>
{

View File

@ -9,8 +9,9 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2"/>
<PackageReference Include="xunit" Version="2.4.2"/>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" />
<PackageReference Include="Moq" Version="4.18.4" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
@ -21,4 +22,9 @@
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\CleanArchitecture.Application\CleanArchitecture.Application.csproj" />
<ProjectReference Include="..\CleanArchitecture.Domain\CleanArchitecture.Domain.csproj" />
</ItemGroup>
</Project>

View File

@ -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<IMediatorHandler> Bus { get; } = new();
public QueryHandlerBaseFixture VerifyExistingNotification(string key, string errorCode, string message)
{
Bus.Verify(
bus => bus.RaiseEventAsync(
It.Is<DomainNotification>(
notification =>
notification.Key == key &&
notification.Code == errorCode &&
notification.Value == message)),
Times.Once);
return this;
}
public QueryHandlerBaseFixture VerifyNoDomainNotification()
{
Bus.Verify(
bus => bus.RaiseEventAsync(It.IsAny<DomainNotification>()),
Times.Never);
return this;
}
}

View File

@ -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<IUserRepository> UserRepository { get; }
public GetAllUsersQueryHandler Handler { get; }
public GetAllUsersTestFixture()
{
UserRepository = new();
Handler = new(UserRepository.Object);
}
}

View File

@ -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<IUserRepository> UserRepository { get; }
public GetUserByIdQueryHandler Handler { get; }
public GetUserByIdTestFixture()
{
UserRepository = new();
Handler = new(UserRepository.Object, Bus.Object);
}
}

View File

@ -0,0 +1,6 @@
namespace CleanArchitecture.Application.Tests.Fixtures.Services;
public sealed class UserServiceTestFixture
{
}

View File

@ -0,0 +1,6 @@
namespace CleanArchitecture.Application.Tests.Queries.Users;
public sealed class GetAllUsersQueryHandlerTests
{
}

View File

@ -0,0 +1,6 @@
namespace CleanArchitecture.Application.Tests.Queries.Users;
public sealed class GetUserByIdQueryHandlerTests
{
}

View File

@ -0,0 +1,6 @@
namespace CleanArchitecture.Application.Tests.Services;
public sealed class UserServiceTests
{
}

View File

@ -1,9 +0,0 @@
namespace CleanArchitecture.Application.Tests;
public class UnitTest1
{
[Fact]
public void Test1()
{
}
}

View File

@ -1 +0,0 @@
global using Xunit;

View File

@ -7,6 +7,7 @@
<ItemGroup>
<PackageReference Include="MediatR" Version="12.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.3" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="7.0.0" />
</ItemGroup>

View File

@ -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<IRequestHandler<GetUserByIdQuery, UserViewModel?>, GetUserByIdQueryHandler>();
services.AddScoped<IRequestHandler<GetAllUsersQuery, IEnumerable<UserViewModel>>, GetAllUsersQueryHandler>();
return services;
}

View File

@ -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<UserViewModel?> GetUserByUserIdAsync(Guid userId);
public Task<IEnumerable<UserViewModel>> GetAllUsersAsync();
}

View File

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

View File

@ -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<GetAllUsersQuery, IEnumerable<UserViewModel>>
{
private readonly IUserRepository _userRepository;
public GetAllUsersQueryHandler(IUserRepository userRepository)
{
_userRepository = userRepository;
}
public async Task<IEnumerable<UserViewModel>> Handle(GetAllUsersQuery request, CancellationToken cancellationToken)
{
return await _userRepository
.GetAllNoTracking()
.Select(x => UserViewModel.FromUser(x))
.ToListAsync(cancellationToken);
}
}

View File

@ -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<IEnumerable<UserViewModel>> GetAllUsersAsync()
{
return await _bus.QueryAsync(new GetAllUsersQuery());
}
}

View File

@ -1,11 +0,0 @@
using Xunit;
namespace CleanArchitecture.Domain.Tests;
public class UnitTest1
{
[Fact]
public void Test1()
{
}
}

View File

@ -10,4 +10,9 @@
<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,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<DomainNotification> notifications)
{
_bus = bus;
_unitOfWork = unitOfWork;
_notifications = (DomainNotificationHandler)notifications;
}
public async Task<bool> 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<bool> 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;
}
}

View File

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

View File

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

View File

@ -0,0 +1,20 @@
using CleanArchitecture.Domain.Errors;
using FluentValidation;
namespace CleanArchitecture.Domain.Commands.Users.DeleteUser;
public sealed class DeleteUserCommandValidation : AbstractValidator<DeleteUserCommand>
{
public DeleteUserCommandValidation()
{
AddRuleForId();
}
private void AddRuleForId()
{
RuleFor(cmd => cmd.UserId)
.NotEmpty()
.WithErrorCode(DomainErrorCodes.UserEmptyId)
.WithMessage("User id may not be empty");
}
}

View File

@ -0,0 +1,6 @@
namespace CleanArchitecture.Domain.Errors;
public static class DomainErrorCodes
{
public const string UserEmptyId = "USER_EMPTY_ID";
}

View File

@ -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<UserDeletedEvent>
{
public Task Handle(UserDeletedEvent notification, CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
}

View File

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

View File

@ -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<IRequestHandler<DeleteUserCommand>, DeleteUserCommandHandler>();
return services;
}
public static IServiceCollection AddNotificationHandlers(this IServiceCollection services)
{
// User
services.AddScoped<INotificationHandler<UserDeletedEvent>, UserEventHandler>();
return services;
}
}

View File

@ -21,4 +21,5 @@ public interface IRepository<TEntity> : IDisposable where TEntity : Entity
void Update(TEntity entity);
Task<bool> ExistsAsync(Guid id);
public void Remove(TEntity entity, bool hardDelete = false);
}

View File

@ -9,8 +9,10 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2"/>
<PackageReference Include="xunit" Version="2.4.2"/>
<PackageReference Include="FluentAssertions" Version="6.10.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" />
<PackageReference Include="Moq" Version="4.18.4" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
@ -21,4 +23,8 @@
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\CleanArchitecture.Infrastructure\CleanArchitecture.Infrastructure.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,16 @@
using CleanArchitecture.Infrastructure.Database;
using Microsoft.Extensions.Logging;
namespace CleanArchitecture.Infrastructure.Tests.Fixtures;
public static class UnitOfWorkTestFixture
{
public static UnitOfWork<ApplicationDbContext> GetUnitOfWork(
ApplicationDbContext dbContext,
ILogger<UnitOfWork<ApplicationDbContext>> logger)
{
var unitOfWork = new UnitOfWork<ApplicationDbContext>(dbContext, logger);
return unitOfWork;
}
}

View File

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

View File

@ -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<ApplicationDbContext>();
var dbContextMock = new Mock<ApplicationDbContext>(options.Options);
var loggerMock = new Mock<ILogger<UnitOfWork<ApplicationDbContext>>>();
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<ApplicationDbContext>();
var dbContextMock = new Mock<ApplicationDbContext>(options.Options);
var loggerMock = new Mock<ILogger<UnitOfWork<ApplicationDbContext>>>();
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<ApplicationDbContext>();
var dbContextMock = new Mock<ApplicationDbContext>(options.Options);
var loggerMock = new Mock<ILogger<UnitOfWork<ApplicationDbContext>>>();
dbContextMock
.Setup(x => x.SaveChangesAsync(CancellationToken.None))
.Throws(new Exception("boom"));
var unitOfWork = UnitOfWorkTestFixture.GetUnitOfWork(dbContextMock.Object, loggerMock.Object);
Func<Task> knalltAction = async () => await unitOfWork.CommitAsync();
await knalltAction.Should().ThrowAsync<Exception>();
}
}

View File

@ -1,9 +0,0 @@
namespace CleanArchitecture.Infrastructure.Tests;
public class UnitTest1
{
[Fact]
public void Test1()
{
}
}

View File

@ -1 +0,0 @@
global using Xunit;

View File

@ -4,7 +4,7 @@ using Microsoft.EntityFrameworkCore;
namespace CleanArchitecture.Infrastructure.Database;
public sealed class ApplicationDbContext : DbContext
public class ApplicationDbContext : DbContext
{
public DbSet<User> Users { get; set; } = null!;

View File

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

View File

@ -1,4 +1,6 @@
- Complete user endpoints
- Add Tests
- Remove warnings and apply suggestions
- Add Docker support
- Add gRPC support
- Make repo public