From 816d92fc854e6477248a72e8a44c4a6beebd44e1 Mon Sep 17 00:00:00 2001 From: alex289 Date: Mon, 28 Aug 2023 19:41:49 +0200 Subject: [PATCH] feat: Add endpoints for tenants --- .../Controllers/TenantController.cs | 75 +++++++++++++++++++ .../Controllers/UserController.cs | 9 +-- .../CleanArchitecture.Application.csproj | 2 + .../Extensions/ServiceCollectionExtension.cs | 9 +++ .../Interfaces/ITenantService.cs | 15 ++++ .../Tenants/GetAll/GetAllTenantsQuery.cs | 7 ++ .../GetAll/GetAllTenantsQueryHandler.cs | 32 ++++++++ .../GetTenantById/GetTenantByIdQuery.cs | 7 ++ .../GetTenantByIdQueryHandler.cs | 45 +++++++++++ .../GetUserById/GetUserByIdQueryHandler.cs | 2 +- .../Services/TenantService.cs | 56 ++++++++++++++ .../Tenants/CreateTenantViewModel.cs | 3 + .../ViewModels/Tenants/TenantViewModel.cs | 24 ++++++ .../Tenants/UpdateTenantViewModel.cs | 7 ++ .../gRPC/UsersApiImplementation.cs | 6 +- .../ChangePasswordCommandHandlerTests.cs | 6 +- .../ChangePasswordCommandValidationTests.cs | 24 +++--- .../CreateUserCommandHandlerTests.cs | 6 +- .../CreateUserCommandValidationTests.cs | 40 +++++----- .../DeleteUserCommandHandlerTests.cs | 4 +- .../DeleteUserCommandValidationTests.cs | 2 +- .../LoginUser/LoginUserCommandHandlerTests.cs | 4 +- .../LoginUserCommandValidationTests.cs | 30 ++++---- .../UpdateUserCommandHandlerTests.cs | 8 +- .../UpdateUserCommandValidationTests.cs | 16 ++-- CleanArchitecture.Domain/ApiUser.cs | 4 +- .../Commands/CommandHandlerBase.cs | 2 +- .../CreateTenant/CreateTenantCommand.cs | 21 ++++++ .../CreateTenantCommandHandler.cs | 58 ++++++++++++++ .../CreateTenantCommandValidation.cs | 33 ++++++++ .../DeleteTenant/DeleteTenantCommand.cs | 18 +++++ .../DeleteTenantCommandHandler.cs | 63 ++++++++++++++++ .../DeleteTenantCommandValidation.cs | 20 +++++ .../UpdateTenant/UpdateTenantCommand.cs | 21 ++++++ .../UpdateTenantCommandHandler.cs | 55 ++++++++++++++ .../UpdateTenantCommandValidation.cs | 33 ++++++++ .../ChangePassword/ChangePasswordCommand.cs | 4 +- .../ChangePasswordCommandHandler.cs | 6 +- .../Users/CreateUser/CreateUserCommand.cs | 4 +- .../CreateUser/CreateUserCommandHandler.cs | 12 +-- .../CreateUser/CreateUserCommandValidation.cs | 14 ++-- .../Users/DeleteUser/DeleteUserCommand.cs | 4 +- .../DeleteUser/DeleteUserCommandHandler.cs | 4 +- .../DeleteUser/DeleteUserCommandValidation.cs | 2 +- .../Users/LoginUser/LoginUserCommand.cs | 4 +- .../LoginUser/LoginUserCommandHandler.cs | 6 +- .../LoginUser/LoginUserCommandValidation.cs | 4 +- .../Users/UpdateUser/UpdateUserCommand.cs | 4 +- .../UpdateUser/UpdateUserCommandHandler.cs | 10 +-- .../UpdateUser/UpdateUserCommandValidation.cs | 16 ++-- CleanArchitecture.Domain/Entities/Tenant.cs | 5 ++ .../Errors/DomainErrorCodes.cs | 54 ++++++++----- .../EventHandler/TenantEventHandler.cs | 27 +++++++ .../Events/Tenant/TenantCreatedEvent.cs | 14 ++++ .../Events/Tenant/TenantDeletedEvent.cs | 11 +++ .../Events/Tenant/TenantUpdatedEvent.cs | 14 ++++ .../Events/User/PasswordChangedEvent.cs | 3 - .../Events/User/UserCreatedEvent.cs | 3 - .../Events/User/UserDeletedEvent.cs | 3 - .../Events/User/UserUpdatedEvent.cs | 3 - .../Extensions/ServiceCollectionExtension.cs | 13 ++++ .../Extensions/Validation/CustomValidator.cs | 14 ++-- .../Interfaces/Repositories/IRepository.cs | 1 + .../Repositories/ITenantRepository.cs | 7 ++ .../EventSourcing/EventStoreContext.cs | 2 +- .../Extensions/ServiceCollectionExtensions.cs | 1 + .../Repositories/BaseRepository.cs | 18 ++++- .../Repositories/TenantRepository.cs | 12 +++ ...ctionalTestsServiceCollectionExtensions.cs | 2 +- .../gRPC/GetUsersByIdsTests.cs | 2 +- .../CleanArchitecture.Proto.csproj | 7 +- CleanArchitecture.Proto/Tenants/Models.proto | 17 +++++ .../Tenants/TenantsApi.proto | 9 +++ CleanArchitecture.Proto/Users/Models.proto | 4 +- CleanArchitecture.Proto/Users/UsersApi.proto | 2 +- .../CleanArchitecture.Shared.csproj | 1 + .../Tenants/TenantViewModel.cs | 7 ++ .../Users/GetUsersByIdsTests.cs | 4 +- CleanArchitecture.gRPC/CleanArchitecture.cs | 8 +- .../Contexts/TenantsContext.cs | 32 ++++++++ .../Contexts/UsersContext.cs | 2 +- .../Extensions/ServiceCollectionExtensions.cs | 13 +++- CleanArchitecture.gRPC/ICleanArchitecture.cs | 1 + .../Interfaces/ITenantsContext.cs | 11 +++ 84 files changed, 998 insertions(+), 190 deletions(-) create mode 100644 CleanArchitecture.Api/Controllers/TenantController.cs create mode 100644 CleanArchitecture.Application/Interfaces/ITenantService.cs create mode 100644 CleanArchitecture.Application/Queries/Tenants/GetAll/GetAllTenantsQuery.cs create mode 100644 CleanArchitecture.Application/Queries/Tenants/GetAll/GetAllTenantsQueryHandler.cs create mode 100644 CleanArchitecture.Application/Queries/Tenants/GetTenantById/GetTenantByIdQuery.cs create mode 100644 CleanArchitecture.Application/Queries/Tenants/GetTenantById/GetTenantByIdQueryHandler.cs create mode 100644 CleanArchitecture.Application/Services/TenantService.cs create mode 100644 CleanArchitecture.Application/ViewModels/Tenants/CreateTenantViewModel.cs create mode 100644 CleanArchitecture.Application/ViewModels/Tenants/TenantViewModel.cs create mode 100644 CleanArchitecture.Application/ViewModels/Tenants/UpdateTenantViewModel.cs create mode 100644 CleanArchitecture.Domain/Commands/Tenants/CreateTenant/CreateTenantCommand.cs create mode 100644 CleanArchitecture.Domain/Commands/Tenants/CreateTenant/CreateTenantCommandHandler.cs create mode 100644 CleanArchitecture.Domain/Commands/Tenants/CreateTenant/CreateTenantCommandValidation.cs create mode 100644 CleanArchitecture.Domain/Commands/Tenants/DeleteTenant/DeleteTenantCommand.cs create mode 100644 CleanArchitecture.Domain/Commands/Tenants/DeleteTenant/DeleteTenantCommandHandler.cs create mode 100644 CleanArchitecture.Domain/Commands/Tenants/DeleteTenant/DeleteTenantCommandValidation.cs create mode 100644 CleanArchitecture.Domain/Commands/Tenants/UpdateTenant/UpdateTenantCommand.cs create mode 100644 CleanArchitecture.Domain/Commands/Tenants/UpdateTenant/UpdateTenantCommandHandler.cs create mode 100644 CleanArchitecture.Domain/Commands/Tenants/UpdateTenant/UpdateTenantCommandValidation.cs create mode 100644 CleanArchitecture.Domain/EventHandler/TenantEventHandler.cs create mode 100644 CleanArchitecture.Domain/Events/Tenant/TenantCreatedEvent.cs create mode 100644 CleanArchitecture.Domain/Events/Tenant/TenantDeletedEvent.cs create mode 100644 CleanArchitecture.Domain/Events/Tenant/TenantUpdatedEvent.cs create mode 100644 CleanArchitecture.Domain/Interfaces/Repositories/ITenantRepository.cs create mode 100644 CleanArchitecture.Infrastructure/Repositories/TenantRepository.cs create mode 100644 CleanArchitecture.Proto/Tenants/Models.proto create mode 100644 CleanArchitecture.Proto/Tenants/TenantsApi.proto create mode 100644 CleanArchitecture.Shared/Tenants/TenantViewModel.cs create mode 100644 CleanArchitecture.gRPC/Contexts/TenantsContext.cs create mode 100644 CleanArchitecture.gRPC/Interfaces/ITenantsContext.cs diff --git a/CleanArchitecture.Api/Controllers/TenantController.cs b/CleanArchitecture.Api/Controllers/TenantController.cs new file mode 100644 index 0000000..639e2c8 --- /dev/null +++ b/CleanArchitecture.Api/Controllers/TenantController.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using CleanArchitecture.Api.Models; +using CleanArchitecture.Application.Interfaces; +using CleanArchitecture.Application.ViewModels.Tenants; +using CleanArchitecture.Domain.Notifications; +using MediatR; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Swashbuckle.AspNetCore.Annotations; + +namespace CleanArchitecture.Api.Controllers; + +[ApiController] +[Authorize] +[Route("/api/v1/[controller]")] +public sealed class TenantController : ApiController +{ + private readonly ITenantService _tenantService; + + public TenantController( + INotificationHandler notifications, + ITenantService tenantService) : base(notifications) + { + _tenantService = tenantService; + } + + [HttpGet] + [SwaggerOperation("Get a list of all tenants")] + [SwaggerResponse(200, "Request successful", typeof(ResponseMessage>))] + public async Task GetAllTenantsAsync() + { + var tenants = await _tenantService.GetAllTenantsAsync(); + return Response(tenants); + } + + [HttpGet("{id:guid}")] + [SwaggerOperation("Get a tenant by id")] + [SwaggerResponse(200, "Request successful", typeof(ResponseMessage))] + public async Task GetTenantByIdAsync( + [FromRoute] Guid id, + [FromQuery] bool isDeleted = false) + { + var tenant = await _tenantService.GetTenantByIdAsync(id, isDeleted); + return Response(tenant); + } + + [HttpPost] + [SwaggerOperation("Create a new tenant")] + [SwaggerResponse(200, "Request successful", typeof(ResponseMessage))] + public async Task CreateTenantAsync([FromBody] CreateTenantViewModel tenant) + { + var tenantId = await _tenantService.CreateTenantAsync(tenant); + return Response(tenantId); + } + + [HttpPut] + [SwaggerOperation("Update an existing tenant")] + [SwaggerResponse(200, "Request successful", typeof(ResponseMessage))] + public async Task UpdateTenantAsync([FromBody] UpdateTenantViewModel tenant) + { + await _tenantService.UpdateTenantAsync(tenant); + return Response(tenant); + } + + [HttpDelete("{id:guid}")] + [SwaggerOperation("Delete an existing tenant")] + [SwaggerResponse(200, "Request successful", typeof(ResponseMessage))] + public async Task DeleteTenantAsync([FromRoute] Guid id) + { + await _tenantService.DeleteTenantAsync(id); + return Response(id); + } +} \ No newline at end of file diff --git a/CleanArchitecture.Api/Controllers/UserController.cs b/CleanArchitecture.Api/Controllers/UserController.cs index b6f7904..aa66217 100644 --- a/CleanArchitecture.Api/Controllers/UserController.cs +++ b/CleanArchitecture.Api/Controllers/UserController.cs @@ -13,6 +13,7 @@ using Swashbuckle.AspNetCore.Annotations; namespace CleanArchitecture.Api.Controllers; [ApiController] +[Authorize] [Route("/api/v1/[controller]")] public sealed class UserController : ApiController { @@ -25,7 +26,6 @@ public sealed class UserController : ApiController _userService = userService; } - [Authorize] [HttpGet] [SwaggerOperation("Get a list of all users")] [SwaggerResponse(200, "Request successful", typeof(ResponseMessage>))] @@ -35,7 +35,6 @@ public sealed class UserController : ApiController return Response(users); } - [Authorize] [HttpGet("{id:guid}")] [SwaggerOperation("Get a user by id")] [SwaggerResponse(200, "Request successful", typeof(ResponseMessage))] @@ -47,7 +46,6 @@ public sealed class UserController : ApiController return Response(user); } - [Authorize] [HttpGet("me")] [SwaggerOperation("Get the current active user")] [SwaggerResponse(200, "Request successful", typeof(ResponseMessage))] @@ -58,6 +56,7 @@ public sealed class UserController : ApiController } [HttpPost] + [AllowAnonymous] [SwaggerOperation("Create a new user")] [SwaggerResponse(200, "Request successful", typeof(ResponseMessage))] public async Task CreateUserAsync([FromBody] CreateUserViewModel viewModel) @@ -66,7 +65,6 @@ public sealed class UserController : ApiController return Response(userId); } - [Authorize] [HttpDelete("{id:guid}")] [SwaggerOperation("Delete a user")] [SwaggerResponse(200, "Request successful", typeof(ResponseMessage))] @@ -76,7 +74,6 @@ public sealed class UserController : ApiController return Response(id); } - [Authorize] [HttpPut] [SwaggerOperation("Update a user")] [SwaggerResponse(200, "Request successful", typeof(ResponseMessage))] @@ -86,7 +83,6 @@ public sealed class UserController : ApiController return Response(viewModel); } - [Authorize] [HttpPost("changePassword")] [SwaggerOperation("Change a password for the current active user")] [SwaggerResponse(200, "Request successful", typeof(ResponseMessage))] @@ -97,6 +93,7 @@ public sealed class UserController : ApiController } [HttpPost("login")] + [AllowAnonymous] [SwaggerOperation("Get a signed token for a user")] [SwaggerResponse(200, "Request successful", typeof(ResponseMessage))] public async Task LoginUserAsync([FromBody] LoginUserViewModel viewModel) diff --git a/CleanArchitecture.Application/CleanArchitecture.Application.csproj b/CleanArchitecture.Application/CleanArchitecture.Application.csproj index fd17662..2c7102f 100644 --- a/CleanArchitecture.Application/CleanArchitecture.Application.csproj +++ b/CleanArchitecture.Application/CleanArchitecture.Application.csproj @@ -14,4 +14,6 @@ + + diff --git a/CleanArchitecture.Application/Extensions/ServiceCollectionExtension.cs b/CleanArchitecture.Application/Extensions/ServiceCollectionExtension.cs index 784380f..03d5044 100644 --- a/CleanArchitecture.Application/Extensions/ServiceCollectionExtension.cs +++ b/CleanArchitecture.Application/Extensions/ServiceCollectionExtension.cs @@ -1,8 +1,11 @@ using System.Collections.Generic; using CleanArchitecture.Application.Interfaces; +using CleanArchitecture.Application.Queries.Tenants.GetAll; +using CleanArchitecture.Application.Queries.Tenants.GetTenantById; using CleanArchitecture.Application.Queries.Users.GetAll; using CleanArchitecture.Application.Queries.Users.GetUserById; using CleanArchitecture.Application.Services; +using CleanArchitecture.Application.ViewModels.Tenants; using CleanArchitecture.Application.ViewModels.Users; using MediatR; using Microsoft.Extensions.DependencyInjection; @@ -14,15 +17,21 @@ public static class ServiceCollectionExtension public static IServiceCollection AddServices(this IServiceCollection services) { services.AddScoped(); + services.AddScoped(); return services; } public static IServiceCollection AddQueryHandlers(this IServiceCollection services) { + // User services.AddScoped, GetUserByIdQueryHandler>(); services.AddScoped>, GetAllUsersQueryHandler>(); + // Tenant + services.AddScoped, GetTenantByIdQueryHandler>(); + services.AddScoped>, GetAllTenantsQueryHandler>(); + return services; } } \ No newline at end of file diff --git a/CleanArchitecture.Application/Interfaces/ITenantService.cs b/CleanArchitecture.Application/Interfaces/ITenantService.cs new file mode 100644 index 0000000..fbf4332 --- /dev/null +++ b/CleanArchitecture.Application/Interfaces/ITenantService.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using CleanArchitecture.Application.ViewModels.Tenants; + +namespace CleanArchitecture.Application.Interfaces; + +public interface ITenantService +{ + public Task CreateTenantAsync(CreateTenantViewModel tenant); + public Task UpdateTenantAsync(UpdateTenantViewModel tenant); + public Task DeleteTenantAsync(Guid tenantId); + public Task GetTenantByIdAsync(Guid tenantId, bool deleted); + public Task> GetAllTenantsAsync(); +} \ No newline at end of file diff --git a/CleanArchitecture.Application/Queries/Tenants/GetAll/GetAllTenantsQuery.cs b/CleanArchitecture.Application/Queries/Tenants/GetAll/GetAllTenantsQuery.cs new file mode 100644 index 0000000..09e81a7 --- /dev/null +++ b/CleanArchitecture.Application/Queries/Tenants/GetAll/GetAllTenantsQuery.cs @@ -0,0 +1,7 @@ +using System.Collections.Generic; +using CleanArchitecture.Application.ViewModels.Tenants; +using MediatR; + +namespace CleanArchitecture.Application.Queries.Tenants.GetAll; + +public sealed record GetAllTenantsQuery() : IRequest>; \ No newline at end of file diff --git a/CleanArchitecture.Application/Queries/Tenants/GetAll/GetAllTenantsQueryHandler.cs b/CleanArchitecture.Application/Queries/Tenants/GetAll/GetAllTenantsQueryHandler.cs new file mode 100644 index 0000000..cd21b8a --- /dev/null +++ b/CleanArchitecture.Application/Queries/Tenants/GetAll/GetAllTenantsQueryHandler.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using CleanArchitecture.Application.ViewModels.Tenants; +using CleanArchitecture.Domain.Interfaces.Repositories; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace CleanArchitecture.Application.Queries.Tenants.GetAll; + +public sealed class GetAllTenantsQueryHandler : + IRequestHandler> +{ + private readonly ITenantRepository _tenantRepository; + + public GetAllTenantsQueryHandler(ITenantRepository tenantRepository) + { + _tenantRepository = tenantRepository; + } + + public async Task> Handle( + GetAllTenantsQuery request, + CancellationToken cancellationToken) + { + return await _tenantRepository + .GetAllNoTracking() + .Where(x => !x.Deleted) + .Select(x => TenantViewModel.FromTenant(x)) + .ToListAsync(cancellationToken); + } +} \ No newline at end of file diff --git a/CleanArchitecture.Application/Queries/Tenants/GetTenantById/GetTenantByIdQuery.cs b/CleanArchitecture.Application/Queries/Tenants/GetTenantById/GetTenantByIdQuery.cs new file mode 100644 index 0000000..8639dfe --- /dev/null +++ b/CleanArchitecture.Application/Queries/Tenants/GetTenantById/GetTenantByIdQuery.cs @@ -0,0 +1,7 @@ +using System; +using CleanArchitecture.Application.ViewModels.Tenants; +using MediatR; + +namespace CleanArchitecture.Application.Queries.Tenants.GetTenantById; + +public sealed record GetTenantByIdQuery(Guid TenantId, bool IsDeleted) : IRequest; \ No newline at end of file diff --git a/CleanArchitecture.Application/Queries/Tenants/GetTenantById/GetTenantByIdQueryHandler.cs b/CleanArchitecture.Application/Queries/Tenants/GetTenantById/GetTenantByIdQueryHandler.cs new file mode 100644 index 0000000..751ab59 --- /dev/null +++ b/CleanArchitecture.Application/Queries/Tenants/GetTenantById/GetTenantByIdQueryHandler.cs @@ -0,0 +1,45 @@ +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using CleanArchitecture.Application.ViewModels.Tenants; +using CleanArchitecture.Domain.Errors; +using CleanArchitecture.Domain.Interfaces; +using CleanArchitecture.Domain.Interfaces.Repositories; +using CleanArchitecture.Domain.Notifications; +using MediatR; + +namespace CleanArchitecture.Application.Queries.Tenants.GetTenantById; + +public sealed class GetTenantByIdQueryHandler : + IRequestHandler +{ + private readonly ITenantRepository _tenantRepository; + private readonly IMediatorHandler _bus; + + public GetTenantByIdQueryHandler(ITenantRepository tenantRepository, IMediatorHandler bus) + { + _tenantRepository = tenantRepository; + _bus = bus; + } + + public async Task Handle(GetTenantByIdQuery request, CancellationToken cancellationToken) + { + var tenant = _tenantRepository + .GetAllNoTracking() + .FirstOrDefault(x => + x.Id == request.TenantId && + x.Deleted == request.IsDeleted); + + if (tenant is null) + { + await _bus.RaiseEventAsync( + new DomainNotification( + nameof(GetTenantByIdQuery), + $"Tenant with id {request.TenantId} could not be found", + ErrorCodes.ObjectNotFound)); + return null; + } + + return TenantViewModel.FromTenant(tenant); + } +} \ No newline at end of file diff --git a/CleanArchitecture.Application/Queries/Users/GetUserById/GetUserByIdQueryHandler.cs b/CleanArchitecture.Application/Queries/Users/GetUserById/GetUserByIdQueryHandler.cs index f057731..aa7974f 100644 --- a/CleanArchitecture.Application/Queries/Users/GetUserById/GetUserByIdQueryHandler.cs +++ b/CleanArchitecture.Application/Queries/Users/GetUserById/GetUserByIdQueryHandler.cs @@ -30,7 +30,7 @@ public sealed class GetUserByIdQueryHandler : x.Id == request.UserId && x.Deleted == request.IsDeleted); - if (user == null) + if (user is null) { await _bus.RaiseEventAsync( new DomainNotification( diff --git a/CleanArchitecture.Application/Services/TenantService.cs b/CleanArchitecture.Application/Services/TenantService.cs new file mode 100644 index 0000000..62279c0 --- /dev/null +++ b/CleanArchitecture.Application/Services/TenantService.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using CleanArchitecture.Application.Interfaces; +using CleanArchitecture.Application.Queries.Tenants.GetAll; +using CleanArchitecture.Application.Queries.Tenants.GetTenantById; +using CleanArchitecture.Application.ViewModels.Tenants; +using CleanArchitecture.Domain.Commands.Tenants.CreateTenant; +using CleanArchitecture.Domain.Commands.Tenants.DeleteTenant; +using CleanArchitecture.Domain.Commands.Tenants.UpdateTenant; +using CleanArchitecture.Domain.Interfaces; + +namespace CleanArchitecture.Application.Services; + +public sealed class TenantService : ITenantService +{ + private readonly IMediatorHandler _bus; + + public TenantService(IMediatorHandler bus) + { + _bus = bus; + } + + public async Task CreateTenantAsync(CreateTenantViewModel tenant) + { + var tenantId = Guid.NewGuid(); + + await _bus.SendCommandAsync(new CreateTenantCommand( + tenantId, + tenant.Name)); + + return tenantId; + } + + public async Task UpdateTenantAsync(UpdateTenantViewModel tenant) + { + await _bus.SendCommandAsync(new UpdateTenantCommand( + tenant.Id, + tenant.Name)); + } + + public async Task DeleteTenantAsync(Guid tenantId) + { + await _bus.SendCommandAsync(new DeleteTenantCommand(tenantId)); + } + + public async Task GetTenantByIdAsync(Guid tenantId, bool deleted) + { + return await _bus.QueryAsync(new GetTenantByIdQuery(tenantId, deleted)); + } + + public async Task> GetAllTenantsAsync() + { + return await _bus.QueryAsync(new GetAllTenantsQuery()); + } +} \ No newline at end of file diff --git a/CleanArchitecture.Application/ViewModels/Tenants/CreateTenantViewModel.cs b/CleanArchitecture.Application/ViewModels/Tenants/CreateTenantViewModel.cs new file mode 100644 index 0000000..e536004 --- /dev/null +++ b/CleanArchitecture.Application/ViewModels/Tenants/CreateTenantViewModel.cs @@ -0,0 +1,3 @@ +namespace CleanArchitecture.Application.ViewModels.Tenants; + +public sealed record CreateTenantViewModel(string Name); \ No newline at end of file diff --git a/CleanArchitecture.Application/ViewModels/Tenants/TenantViewModel.cs b/CleanArchitecture.Application/ViewModels/Tenants/TenantViewModel.cs new file mode 100644 index 0000000..586768f --- /dev/null +++ b/CleanArchitecture.Application/ViewModels/Tenants/TenantViewModel.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using CleanArchitecture.Application.ViewModels.Users; +using CleanArchitecture.Domain.Entities; + +namespace CleanArchitecture.Application.ViewModels.Tenants; + +public sealed class TenantViewModel +{ + public Guid Id { get; set; } + public string Name { get; set; } = string.Empty; + public IEnumerable Users { get; set; } = new List(); + + public static TenantViewModel FromTenant(Tenant tenant) + { + return new TenantViewModel + { + Id = tenant.Id, + Name = tenant.Name, + Users = tenant.Users.Select(UserViewModel.FromUser) + }; + } +} \ No newline at end of file diff --git a/CleanArchitecture.Application/ViewModels/Tenants/UpdateTenantViewModel.cs b/CleanArchitecture.Application/ViewModels/Tenants/UpdateTenantViewModel.cs new file mode 100644 index 0000000..72e595e --- /dev/null +++ b/CleanArchitecture.Application/ViewModels/Tenants/UpdateTenantViewModel.cs @@ -0,0 +1,7 @@ +using System; + +namespace CleanArchitecture.Application.ViewModels.Tenants; + +public sealed record UpdateTenantViewModel( + Guid Id, + string Name); \ No newline at end of file diff --git a/CleanArchitecture.Application/gRPC/UsersApiImplementation.cs b/CleanArchitecture.Application/gRPC/UsersApiImplementation.cs index d8f9599..ff872e1 100644 --- a/CleanArchitecture.Application/gRPC/UsersApiImplementation.cs +++ b/CleanArchitecture.Application/gRPC/UsersApiImplementation.cs @@ -18,8 +18,8 @@ public sealed class UsersApiImplementation : UsersApi.UsersApiBase _userRepository = userRepository; } - public override async Task GetByIds( - GetByIdsRequest request, + public override async Task GetByIds( + GetUsersByIdsRequest request, ServerCallContext context) { var idsAsGuids = new List(request.Ids.Count); @@ -45,7 +45,7 @@ public sealed class UsersApiImplementation : UsersApi.UsersApiBase }) .ToListAsync(); - var result = new GetByIdsResult(); + var result = new GetUsersByIdsResult(); result.Users.AddRange(users); diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/User/ChangePassword/ChangePasswordCommandHandlerTests.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/ChangePassword/ChangePasswordCommandHandlerTests.cs index 08d16ca..293c603 100644 --- a/CleanArchitecture.Domain.Tests/CommandHandler/User/ChangePassword/ChangePasswordCommandHandlerTests.cs +++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/ChangePassword/ChangePasswordCommandHandlerTests.cs @@ -22,7 +22,7 @@ public sealed class ChangePasswordCommandHandlerTests _fixture .VerifyNoDomainNotification() .VerifyCommit() - .VerifyRaisedEvent(x => x.UserId == user.Id); + .VerifyRaisedEvent(x => x.AggregateId == user.Id); } [Fact] @@ -40,7 +40,7 @@ public sealed class ChangePasswordCommandHandlerTests .VerifyAnyDomainNotification() .VerifyExistingNotification( ErrorCodes.ObjectNotFound, - $"There is no User with Id {userId}"); + $"There is no user with Id {userId}"); } [Fact] @@ -57,7 +57,7 @@ public sealed class ChangePasswordCommandHandlerTests .VerifyNoRaisedEvent() .VerifyAnyDomainNotification() .VerifyExistingNotification( - DomainErrorCodes.UserPasswordIncorrect, + DomainErrorCodes.User.UserPasswordIncorrect, "The password is incorrect"); } } \ No newline at end of file diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/User/ChangePassword/ChangePasswordCommandValidationTests.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/ChangePassword/ChangePasswordCommandValidationTests.cs index 39c8186..4a1a41a 100644 --- a/CleanArchitecture.Domain.Tests/CommandHandler/User/ChangePassword/ChangePasswordCommandValidationTests.cs +++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/ChangePassword/ChangePasswordCommandValidationTests.cs @@ -28,12 +28,12 @@ public sealed class ChangePasswordCommandValidationTests : var errors = new List { - DomainErrorCodes.UserEmptyPassword, - DomainErrorCodes.UserSpecialCharPassword, - DomainErrorCodes.UserNumberPassword, - DomainErrorCodes.UserLowercaseLetterPassword, - DomainErrorCodes.UserUppercaseLetterPassword, - DomainErrorCodes.UserShortPassword + DomainErrorCodes.User.UserEmptyPassword, + DomainErrorCodes.User.UserSpecialCharPassword, + DomainErrorCodes.User.UserNumberPassword, + DomainErrorCodes.User.UserLowercaseLetterPassword, + DomainErrorCodes.User.UserUppercaseLetterPassword, + DomainErrorCodes.User.UserShortPassword }; ShouldHaveExpectedErrors(command, errors.ToArray()); @@ -44,7 +44,7 @@ public sealed class ChangePasswordCommandValidationTests : { var command = CreateTestCommand("z8tnayvd5FNLU9AQm"); - ShouldHaveSingleError(command, DomainErrorCodes.UserSpecialCharPassword); + ShouldHaveSingleError(command, DomainErrorCodes.User.UserSpecialCharPassword); } [Fact] @@ -52,7 +52,7 @@ public sealed class ChangePasswordCommandValidationTests : { var command = CreateTestCommand("z]tnayvdFNLU:]AQm"); - ShouldHaveSingleError(command, DomainErrorCodes.UserNumberPassword); + ShouldHaveSingleError(command, DomainErrorCodes.User.UserNumberPassword); } [Fact] @@ -60,7 +60,7 @@ public sealed class ChangePasswordCommandValidationTests : { var command = CreateTestCommand("Z8]TNAYVDFNLU:]AQM"); - ShouldHaveSingleError(command, DomainErrorCodes.UserLowercaseLetterPassword); + ShouldHaveSingleError(command, DomainErrorCodes.User.UserLowercaseLetterPassword); } [Fact] @@ -68,7 +68,7 @@ public sealed class ChangePasswordCommandValidationTests : { var command = CreateTestCommand("z8]tnayvd5fnlu9:]aqm"); - ShouldHaveSingleError(command, DomainErrorCodes.UserUppercaseLetterPassword); + ShouldHaveSingleError(command, DomainErrorCodes.User.UserUppercaseLetterPassword); } [Fact] @@ -76,7 +76,7 @@ public sealed class ChangePasswordCommandValidationTests : { var command = CreateTestCommand("zA6{"); - ShouldHaveSingleError(command, DomainErrorCodes.UserShortPassword); + ShouldHaveSingleError(command, DomainErrorCodes.User.UserShortPassword); } [Fact] @@ -84,7 +84,7 @@ public sealed class ChangePasswordCommandValidationTests : { var command = CreateTestCommand(string.Concat(Enumerable.Repeat("zA6{", 12), 12)); - ShouldHaveSingleError(command, DomainErrorCodes.UserLongPassword); + ShouldHaveSingleError(command, DomainErrorCodes.User.UserLongPassword); } private static ChangePasswordCommand CreateTestCommand( diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandHandlerTests.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandHandlerTests.cs index b902a98..14fcb04 100644 --- a/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandHandlerTests.cs +++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandHandlerTests.cs @@ -28,7 +28,7 @@ public sealed class CreateUserCommandHandlerTests _fixture .VerifyNoDomainNotification() .VerifyCommit() - .VerifyRaisedEvent(x => x.UserId == command.UserId); + .VerifyRaisedEvent(x => x.AggregateId == command.UserId); } [Fact] @@ -51,7 +51,7 @@ public sealed class CreateUserCommandHandlerTests .VerifyNoRaisedEvent() .VerifyAnyDomainNotification() .VerifyExistingNotification( - DomainErrorCodes.UserAlreadyExists, - $"There is already a User with Id {command.UserId}"); + DomainErrorCodes.User.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/CreateUserCommandValidationTests.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandValidationTests.cs index 7906a1f..3e303ec 100644 --- a/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandValidationTests.cs +++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/CreateUser/CreateUserCommandValidationTests.cs @@ -29,7 +29,7 @@ public sealed class CreateUserCommandValidationTests : ShouldHaveSingleError( command, - DomainErrorCodes.UserEmptyId, + DomainErrorCodes.User.UserEmptyId, "User id may not be empty"); } @@ -40,7 +40,7 @@ public sealed class CreateUserCommandValidationTests : ShouldHaveSingleError( command, - DomainErrorCodes.UserInvalidEmail, + DomainErrorCodes.User.UserInvalidEmail, "Email is not a valid email address"); } @@ -51,7 +51,7 @@ public sealed class CreateUserCommandValidationTests : ShouldHaveSingleError( command, - DomainErrorCodes.UserInvalidEmail, + DomainErrorCodes.User.UserInvalidEmail, "Email is not a valid email address"); } @@ -62,7 +62,7 @@ public sealed class CreateUserCommandValidationTests : ShouldHaveSingleError( command, - DomainErrorCodes.UserEmailExceedsMaxLength, + DomainErrorCodes.User.UserEmailExceedsMaxLength, "Email may not be longer than 320 characters"); } @@ -73,7 +73,7 @@ public sealed class CreateUserCommandValidationTests : ShouldHaveSingleError( command, - DomainErrorCodes.UserEmptyFirstName, + DomainErrorCodes.User.UserEmptyFirstName, "FirstName may not be empty"); } @@ -84,7 +84,7 @@ public sealed class CreateUserCommandValidationTests : ShouldHaveSingleError( command, - DomainErrorCodes.UserFirstNameExceedsMaxLength, + DomainErrorCodes.User.UserFirstNameExceedsMaxLength, "FirstName may not be longer than 100 characters"); } @@ -95,7 +95,7 @@ public sealed class CreateUserCommandValidationTests : ShouldHaveSingleError( command, - DomainErrorCodes.UserEmptyLastName, + DomainErrorCodes.User.UserEmptyLastName, "LastName may not be empty"); } @@ -106,7 +106,7 @@ public sealed class CreateUserCommandValidationTests : ShouldHaveSingleError( command, - DomainErrorCodes.UserLastNameExceedsMaxLength, + DomainErrorCodes.User.UserLastNameExceedsMaxLength, "LastName may not be longer than 100 characters"); } @@ -117,12 +117,12 @@ public sealed class CreateUserCommandValidationTests : var errors = new List { - DomainErrorCodes.UserEmptyPassword, - DomainErrorCodes.UserSpecialCharPassword, - DomainErrorCodes.UserNumberPassword, - DomainErrorCodes.UserLowercaseLetterPassword, - DomainErrorCodes.UserUppercaseLetterPassword, - DomainErrorCodes.UserShortPassword + DomainErrorCodes.User.UserEmptyPassword, + DomainErrorCodes.User.UserSpecialCharPassword, + DomainErrorCodes.User.UserNumberPassword, + DomainErrorCodes.User.UserLowercaseLetterPassword, + DomainErrorCodes.User.UserUppercaseLetterPassword, + DomainErrorCodes.User.UserShortPassword }; ShouldHaveExpectedErrors(command, errors.ToArray()); @@ -133,7 +133,7 @@ public sealed class CreateUserCommandValidationTests : { var command = CreateTestCommand(password: "z8tnayvd5FNLU9AQm"); - ShouldHaveSingleError(command, DomainErrorCodes.UserSpecialCharPassword); + ShouldHaveSingleError(command, DomainErrorCodes.User.UserSpecialCharPassword); } [Fact] @@ -141,7 +141,7 @@ public sealed class CreateUserCommandValidationTests : { var command = CreateTestCommand(password: "z]tnayvdFNLU:]AQm"); - ShouldHaveSingleError(command, DomainErrorCodes.UserNumberPassword); + ShouldHaveSingleError(command, DomainErrorCodes.User.UserNumberPassword); } [Fact] @@ -149,7 +149,7 @@ public sealed class CreateUserCommandValidationTests : { var command = CreateTestCommand(password: "Z8]TNAYVDFNLU:]AQM"); - ShouldHaveSingleError(command, DomainErrorCodes.UserLowercaseLetterPassword); + ShouldHaveSingleError(command, DomainErrorCodes.User.UserLowercaseLetterPassword); } [Fact] @@ -157,7 +157,7 @@ public sealed class CreateUserCommandValidationTests : { var command = CreateTestCommand(password: "z8]tnayvd5fnlu9:]aqm"); - ShouldHaveSingleError(command, DomainErrorCodes.UserUppercaseLetterPassword); + ShouldHaveSingleError(command, DomainErrorCodes.User.UserUppercaseLetterPassword); } [Fact] @@ -165,7 +165,7 @@ public sealed class CreateUserCommandValidationTests : { var command = CreateTestCommand(password: "zA6{"); - ShouldHaveSingleError(command, DomainErrorCodes.UserShortPassword); + ShouldHaveSingleError(command, DomainErrorCodes.User.UserShortPassword); } [Fact] @@ -173,7 +173,7 @@ public sealed class CreateUserCommandValidationTests : { var command = CreateTestCommand(password: string.Concat(Enumerable.Repeat("zA6{", 12), 12)); - ShouldHaveSingleError(command, DomainErrorCodes.UserLongPassword); + ShouldHaveSingleError(command, DomainErrorCodes.User.UserLongPassword); } private static CreateUserCommand CreateTestCommand( diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/User/DeleteUser/DeleteUserCommandHandlerTests.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/DeleteUser/DeleteUserCommandHandlerTests.cs index fabfd29..1e65782 100644 --- a/CleanArchitecture.Domain.Tests/CommandHandler/User/DeleteUser/DeleteUserCommandHandlerTests.cs +++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/DeleteUser/DeleteUserCommandHandlerTests.cs @@ -22,7 +22,7 @@ public sealed class DeleteUserCommandHandlerTests _fixture .VerifyNoDomainNotification() .VerifyCommit() - .VerifyRaisedEvent(x => x.UserId == user.Id); + .VerifyRaisedEvent(x => x.AggregateId == user.Id); } [Fact] @@ -40,6 +40,6 @@ public sealed class DeleteUserCommandHandlerTests .VerifyAnyDomainNotification() .VerifyExistingNotification( ErrorCodes.ObjectNotFound, - $"There is no User with Id {command.UserId}"); + $"There is no user with Id {command.UserId}"); } } \ 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 index a2e6e45..9464154 100644 --- a/CleanArchitecture.Domain.Tests/CommandHandler/User/DeleteUser/DeleteUserCommandValidationTests.cs +++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/DeleteUser/DeleteUserCommandValidationTests.cs @@ -27,7 +27,7 @@ public sealed class DeleteUserCommandValidationTests : ShouldHaveSingleError( command, - DomainErrorCodes.UserEmptyId, + DomainErrorCodes.User.UserEmptyId, "User id may not be empty"); } diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/User/LoginUser/LoginUserCommandHandlerTests.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/LoginUser/LoginUserCommandHandlerTests.cs index ba4deaa..1c26051 100644 --- a/CleanArchitecture.Domain.Tests/CommandHandler/User/LoginUser/LoginUserCommandHandlerTests.cs +++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/LoginUser/LoginUserCommandHandlerTests.cs @@ -57,7 +57,7 @@ public sealed class LoginUserCommandHandlerTests .VerifyAnyDomainNotification() .VerifyExistingNotification( ErrorCodes.ObjectNotFound, - $"There is no User with Email {command.Email}"); + $"There is no user with email {command.Email}"); token.Should().BeEmpty(); } @@ -74,7 +74,7 @@ public sealed class LoginUserCommandHandlerTests _fixture .VerifyAnyDomainNotification() .VerifyExistingNotification( - DomainErrorCodes.UserPasswordIncorrect, + DomainErrorCodes.User.UserPasswordIncorrect, "The password is incorrect"); token.Should().BeEmpty(); diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/User/LoginUser/LoginUserCommandValidationTests.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/LoginUser/LoginUserCommandValidationTests.cs index fd85ce8..d875b73 100644 --- a/CleanArchitecture.Domain.Tests/CommandHandler/User/LoginUser/LoginUserCommandValidationTests.cs +++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/LoginUser/LoginUserCommandValidationTests.cs @@ -28,7 +28,7 @@ public sealed class LoginUserCommandValidationTests : ShouldHaveSingleError( command, - DomainErrorCodes.UserInvalidEmail, + DomainErrorCodes.User.UserInvalidEmail, "Email is not a valid email address"); } @@ -39,7 +39,7 @@ public sealed class LoginUserCommandValidationTests : ShouldHaveSingleError( command, - DomainErrorCodes.UserInvalidEmail, + DomainErrorCodes.User.UserInvalidEmail, "Email is not a valid email address"); } @@ -50,7 +50,7 @@ public sealed class LoginUserCommandValidationTests : ShouldHaveSingleError( command, - DomainErrorCodes.UserEmailExceedsMaxLength, + DomainErrorCodes.User.UserEmailExceedsMaxLength, "Email may not be longer than 320 characters"); } @@ -61,12 +61,12 @@ public sealed class LoginUserCommandValidationTests : var errors = new List { - DomainErrorCodes.UserEmptyPassword, - DomainErrorCodes.UserSpecialCharPassword, - DomainErrorCodes.UserNumberPassword, - DomainErrorCodes.UserLowercaseLetterPassword, - DomainErrorCodes.UserUppercaseLetterPassword, - DomainErrorCodes.UserShortPassword + DomainErrorCodes.User.UserEmptyPassword, + DomainErrorCodes.User.UserSpecialCharPassword, + DomainErrorCodes.User.UserNumberPassword, + DomainErrorCodes.User.UserLowercaseLetterPassword, + DomainErrorCodes.User.UserUppercaseLetterPassword, + DomainErrorCodes.User.UserShortPassword }; ShouldHaveExpectedErrors(command, errors.ToArray()); @@ -77,7 +77,7 @@ public sealed class LoginUserCommandValidationTests : { var command = CreateTestCommand(password: "z8tnayvd5FNLU9AQm"); - ShouldHaveSingleError(command, DomainErrorCodes.UserSpecialCharPassword); + ShouldHaveSingleError(command, DomainErrorCodes.User.UserSpecialCharPassword); } [Fact] @@ -85,7 +85,7 @@ public sealed class LoginUserCommandValidationTests : { var command = CreateTestCommand(password: "z]tnayvdFNLU:]AQm"); - ShouldHaveSingleError(command, DomainErrorCodes.UserNumberPassword); + ShouldHaveSingleError(command, DomainErrorCodes.User.UserNumberPassword); } [Fact] @@ -93,7 +93,7 @@ public sealed class LoginUserCommandValidationTests : { var command = CreateTestCommand(password: "Z8]TNAYVDFNLU:]AQM"); - ShouldHaveSingleError(command, DomainErrorCodes.UserLowercaseLetterPassword); + ShouldHaveSingleError(command, DomainErrorCodes.User.UserLowercaseLetterPassword); } [Fact] @@ -101,7 +101,7 @@ public sealed class LoginUserCommandValidationTests : { var command = CreateTestCommand(password: "z8]tnayvd5fnlu9:]aqm"); - ShouldHaveSingleError(command, DomainErrorCodes.UserUppercaseLetterPassword); + ShouldHaveSingleError(command, DomainErrorCodes.User.UserUppercaseLetterPassword); } [Fact] @@ -109,7 +109,7 @@ public sealed class LoginUserCommandValidationTests : { var command = CreateTestCommand(password: "zA6{"); - ShouldHaveSingleError(command, DomainErrorCodes.UserShortPassword); + ShouldHaveSingleError(command, DomainErrorCodes.User.UserShortPassword); } [Fact] @@ -117,7 +117,7 @@ public sealed class LoginUserCommandValidationTests : { var command = CreateTestCommand(password: string.Concat(Enumerable.Repeat("zA6{", 12), 12)); - ShouldHaveSingleError(command, DomainErrorCodes.UserLongPassword); + ShouldHaveSingleError(command, DomainErrorCodes.User.UserLongPassword); } private static LoginUserCommand CreateTestCommand( diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/User/UpdateUser/UpdateUserCommandHandlerTests.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/UpdateUser/UpdateUserCommandHandlerTests.cs index 66281d3..4bcb809 100644 --- a/CleanArchitecture.Domain.Tests/CommandHandler/User/UpdateUser/UpdateUserCommandHandlerTests.cs +++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/UpdateUser/UpdateUserCommandHandlerTests.cs @@ -30,7 +30,7 @@ public sealed class UpdateUserCommandHandlerTests _fixture .VerifyNoDomainNotification() .VerifyCommit() - .VerifyRaisedEvent(x => x.UserId == command.UserId); + .VerifyRaisedEvent(x => x.AggregateId == command.UserId); } [Fact] @@ -53,7 +53,7 @@ public sealed class UpdateUserCommandHandlerTests .VerifyAnyDomainNotification() .VerifyExistingNotification( ErrorCodes.ObjectNotFound, - $"There is no User with Id {command.UserId}"); + $"There is no user with Id {command.UserId}"); } [Fact] @@ -86,7 +86,7 @@ public sealed class UpdateUserCommandHandlerTests .VerifyNoRaisedEvent() .VerifyAnyDomainNotification() .VerifyExistingNotification( - DomainErrorCodes.UserAlreadyExists, - $"There is already a User with Email {command.Email}"); + DomainErrorCodes.User.UserAlreadyExists, + $"There is already a user with email {command.Email}"); } } \ 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 index 7a919f4..762dcb1 100644 --- a/CleanArchitecture.Domain.Tests/CommandHandler/User/UpdateUser/UpdateUserCommandValidationTests.cs +++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/UpdateUser/UpdateUserCommandValidationTests.cs @@ -28,7 +28,7 @@ public sealed class UpdateUserCommandValidationTests : ShouldHaveSingleError( command, - DomainErrorCodes.UserEmptyId, + DomainErrorCodes.User.UserEmptyId, "User id may not be empty"); } @@ -39,7 +39,7 @@ public sealed class UpdateUserCommandValidationTests : ShouldHaveSingleError( command, - DomainErrorCodes.UserInvalidEmail, + DomainErrorCodes.User.UserInvalidEmail, "Email is not a valid email address"); } @@ -50,7 +50,7 @@ public sealed class UpdateUserCommandValidationTests : ShouldHaveSingleError( command, - DomainErrorCodes.UserInvalidEmail, + DomainErrorCodes.User.UserInvalidEmail, "Email is not a valid email address"); } @@ -61,7 +61,7 @@ public sealed class UpdateUserCommandValidationTests : ShouldHaveSingleError( command, - DomainErrorCodes.UserEmailExceedsMaxLength, + DomainErrorCodes.User.UserEmailExceedsMaxLength, "Email may not be longer than 320 characters"); } @@ -72,7 +72,7 @@ public sealed class UpdateUserCommandValidationTests : ShouldHaveSingleError( command, - DomainErrorCodes.UserEmptyFirstName, + DomainErrorCodes.User.UserEmptyFirstName, "FirstName may not be empty"); } @@ -83,7 +83,7 @@ public sealed class UpdateUserCommandValidationTests : ShouldHaveSingleError( command, - DomainErrorCodes.UserFirstNameExceedsMaxLength, + DomainErrorCodes.User.UserFirstNameExceedsMaxLength, "FirstName may not be longer than 100 characters"); } @@ -94,7 +94,7 @@ public sealed class UpdateUserCommandValidationTests : ShouldHaveSingleError( command, - DomainErrorCodes.UserEmptyLastName, + DomainErrorCodes.User.UserEmptyLastName, "LastName may not be empty"); } @@ -105,7 +105,7 @@ public sealed class UpdateUserCommandValidationTests : ShouldHaveSingleError( command, - DomainErrorCodes.UserLastNameExceedsMaxLength, + DomainErrorCodes.User.UserLastNameExceedsMaxLength, "LastName may not be longer than 100 characters"); } diff --git a/CleanArchitecture.Domain/ApiUser.cs b/CleanArchitecture.Domain/ApiUser.cs index 708cec4..df1db8a 100644 --- a/CleanArchitecture.Domain/ApiUser.cs +++ b/CleanArchitecture.Domain/ApiUser.cs @@ -55,12 +55,12 @@ public sealed class ApiUser : IUser { get { - if (_name != null) + if (_name is not null) { return _name; } var identity = _httpContextAccessor.HttpContext?.User.Identity; - if (identity == null) + if (identity is null) { _name = string.Empty; return string.Empty; diff --git a/CleanArchitecture.Domain/Commands/CommandHandlerBase.cs b/CleanArchitecture.Domain/Commands/CommandHandlerBase.cs index 54a557b..6ab6b8f 100644 --- a/CleanArchitecture.Domain/Commands/CommandHandlerBase.cs +++ b/CleanArchitecture.Domain/Commands/CommandHandlerBase.cs @@ -62,7 +62,7 @@ public abstract class CommandHandlerBase return true; } - if (command.ValidationResult == null) + if (command.ValidationResult is null) { throw new InvalidOperationException("Command is invalid and should therefore have a validation result"); } diff --git a/CleanArchitecture.Domain/Commands/Tenants/CreateTenant/CreateTenantCommand.cs b/CleanArchitecture.Domain/Commands/Tenants/CreateTenant/CreateTenantCommand.cs new file mode 100644 index 0000000..3a97121 --- /dev/null +++ b/CleanArchitecture.Domain/Commands/Tenants/CreateTenant/CreateTenantCommand.cs @@ -0,0 +1,21 @@ +using System; + +namespace CleanArchitecture.Domain.Commands.Tenants.CreateTenant; + +public sealed class CreateTenantCommand : CommandBase +{ + private static readonly CreateTenantCommandValidation s_validation = new(); + + public string Name { get; } + + public CreateTenantCommand(Guid tenantId, string name) : base(tenantId) + { + Name = name; + } + + public override bool IsValid() + { + ValidationResult = s_validation.Validate(this); + return ValidationResult.IsValid; + } +} \ No newline at end of file diff --git a/CleanArchitecture.Domain/Commands/Tenants/CreateTenant/CreateTenantCommandHandler.cs b/CleanArchitecture.Domain/Commands/Tenants/CreateTenant/CreateTenantCommandHandler.cs new file mode 100644 index 0000000..78c4aa1 --- /dev/null +++ b/CleanArchitecture.Domain/Commands/Tenants/CreateTenant/CreateTenantCommandHandler.cs @@ -0,0 +1,58 @@ +using System.Threading; +using System.Threading.Tasks; +using CleanArchitecture.Domain.Entities; +using CleanArchitecture.Domain.Errors; +using CleanArchitecture.Domain.Events.Tenant; +using CleanArchitecture.Domain.Interfaces; +using CleanArchitecture.Domain.Interfaces.Repositories; +using CleanArchitecture.Domain.Notifications; +using MediatR; + +namespace CleanArchitecture.Domain.Commands.Tenants.CreateTenant; + +public sealed class CreateTenantCommandHandler : CommandHandlerBase, + IRequestHandler +{ + private readonly ITenantRepository _tenantRepository; + + public CreateTenantCommandHandler( + IMediatorHandler bus, + IUnitOfWork unitOfWork, + INotificationHandler notifications, + ITenantRepository tenantRepository) : base(bus, unitOfWork, notifications) + { + _tenantRepository = tenantRepository; + } + + public async Task Handle(CreateTenantCommand request, CancellationToken cancellationToken) + { + if (!await TestValidityAsync(request)) + { + return; + } + + if (await _tenantRepository.ExistsAsync(request.AggregateId)) + { + await NotifyAsync( + new DomainNotification( + request.MessageType, + $"There is already a tenant with Id {request.AggregateId}", + DomainErrorCodes.Tenant.TenantAlreadyExists)); + + return; + } + + var tenant = new Tenant( + request.AggregateId, + request.Name); + + _tenantRepository.Add(tenant); + + if (await CommitAsync()) + { + await Bus.RaiseEventAsync(new TenantCreatedEvent( + tenant.Id, + tenant.Name)); + } + } +} \ No newline at end of file diff --git a/CleanArchitecture.Domain/Commands/Tenants/CreateTenant/CreateTenantCommandValidation.cs b/CleanArchitecture.Domain/Commands/Tenants/CreateTenant/CreateTenantCommandValidation.cs new file mode 100644 index 0000000..4838663 --- /dev/null +++ b/CleanArchitecture.Domain/Commands/Tenants/CreateTenant/CreateTenantCommandValidation.cs @@ -0,0 +1,33 @@ +using CleanArchitecture.Domain.Constants; +using CleanArchitecture.Domain.Errors; +using FluentValidation; + +namespace CleanArchitecture.Domain.Commands.Tenants.CreateTenant; + +public sealed class CreateTenantCommandValidation : AbstractValidator +{ + public CreateTenantCommandValidation() + { + AddRuleForId(); + AddRuleForName(); + } + + private void AddRuleForId() + { + RuleFor(cmd => cmd.AggregateId) + .NotEmpty() + .WithErrorCode(DomainErrorCodes.Tenant.TenantEmptyId) + .WithMessage("Tenant id may not be empty"); + } + + private void AddRuleForName() + { + RuleFor(cmd => cmd.Name) + .NotEmpty() + .WithErrorCode(DomainErrorCodes.Tenant.TenantEmptyName) + .WithMessage("Name may not be empty") + .MaximumLength(MaxLengths.Tenant.Name) + .WithErrorCode(DomainErrorCodes.Tenant.TenantNameExceedsMaxLength) + .WithMessage($"Name may not be longer than {MaxLengths.Tenant.Name} characters"); + } +} \ No newline at end of file diff --git a/CleanArchitecture.Domain/Commands/Tenants/DeleteTenant/DeleteTenantCommand.cs b/CleanArchitecture.Domain/Commands/Tenants/DeleteTenant/DeleteTenantCommand.cs new file mode 100644 index 0000000..9e112ca --- /dev/null +++ b/CleanArchitecture.Domain/Commands/Tenants/DeleteTenant/DeleteTenantCommand.cs @@ -0,0 +1,18 @@ +using System; + +namespace CleanArchitecture.Domain.Commands.Tenants.DeleteTenant; + +public sealed class DeleteTenantCommand : CommandBase +{ + private static readonly DeleteTenantCommandValidation s_validation = new(); + + public DeleteTenantCommand(Guid tenantId) : base(tenantId) + { + } + + public override bool IsValid() + { + ValidationResult = s_validation.Validate(this); + return ValidationResult.IsValid; + } +} \ No newline at end of file diff --git a/CleanArchitecture.Domain/Commands/Tenants/DeleteTenant/DeleteTenantCommandHandler.cs b/CleanArchitecture.Domain/Commands/Tenants/DeleteTenant/DeleteTenantCommandHandler.cs new file mode 100644 index 0000000..1a1ab43 --- /dev/null +++ b/CleanArchitecture.Domain/Commands/Tenants/DeleteTenant/DeleteTenantCommandHandler.cs @@ -0,0 +1,63 @@ +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using CleanArchitecture.Domain.Errors; +using CleanArchitecture.Domain.Events.Tenant; +using CleanArchitecture.Domain.Interfaces; +using CleanArchitecture.Domain.Interfaces.Repositories; +using CleanArchitecture.Domain.Notifications; +using MediatR; + +namespace CleanArchitecture.Domain.Commands.Tenants.DeleteTenant; + +public sealed class DeleteTenantCommandHandler : CommandHandlerBase, + IRequestHandler +{ + private readonly ITenantRepository _tenantRepository; + private readonly IUserRepository _userRepository; + + public DeleteTenantCommandHandler( + IMediatorHandler bus, + IUnitOfWork unitOfWork, + INotificationHandler notifications, + ITenantRepository tenantRepository, + IUserRepository userRepository) : base(bus, unitOfWork, notifications) + { + _tenantRepository = tenantRepository; + _userRepository = userRepository; + } + + public async Task Handle(DeleteTenantCommand request, CancellationToken cancellationToken) + { + if (!await TestValidityAsync(request)) + { + return; + } + + var tenant = await _tenantRepository.GetByIdAsync(request.AggregateId); + + if (tenant is null) + { + await NotifyAsync( + new DomainNotification( + request.MessageType, + $"There is no tenant with Id {request.AggregateId}", + ErrorCodes.ObjectNotFound)); + + return; + } + + var tenantUsers = _userRepository + .GetAll() + .Where(x => x.TenantId == request.AggregateId); + + _userRepository.RemoveRange(tenantUsers); + + _tenantRepository.Remove(tenant); + + if (await CommitAsync()) + { + await Bus.RaiseEventAsync(new TenantDeletedEvent(tenant.Id)); + } + } +} \ No newline at end of file diff --git a/CleanArchitecture.Domain/Commands/Tenants/DeleteTenant/DeleteTenantCommandValidation.cs b/CleanArchitecture.Domain/Commands/Tenants/DeleteTenant/DeleteTenantCommandValidation.cs new file mode 100644 index 0000000..dadddd8 --- /dev/null +++ b/CleanArchitecture.Domain/Commands/Tenants/DeleteTenant/DeleteTenantCommandValidation.cs @@ -0,0 +1,20 @@ +using CleanArchitecture.Domain.Errors; +using FluentValidation; + +namespace CleanArchitecture.Domain.Commands.Tenants.DeleteTenant; + +public sealed class DeleteTenantCommandValidation : AbstractValidator +{ + public DeleteTenantCommandValidation() + { + AddRuleForId(); + } + + private void AddRuleForId() + { + RuleFor(cmd => cmd.AggregateId) + .NotEmpty() + .WithErrorCode(DomainErrorCodes.Tenant.TenantEmptyId) + .WithMessage("Tenant id may not be empty"); + } +} \ No newline at end of file diff --git a/CleanArchitecture.Domain/Commands/Tenants/UpdateTenant/UpdateTenantCommand.cs b/CleanArchitecture.Domain/Commands/Tenants/UpdateTenant/UpdateTenantCommand.cs new file mode 100644 index 0000000..3962e9b --- /dev/null +++ b/CleanArchitecture.Domain/Commands/Tenants/UpdateTenant/UpdateTenantCommand.cs @@ -0,0 +1,21 @@ +using System; + +namespace CleanArchitecture.Domain.Commands.Tenants.UpdateTenant; + +public sealed class UpdateTenantCommand : CommandBase +{ + private static readonly UpdateTenantCommandValidation s_validation = new(); + + public string Name { get; } + + public UpdateTenantCommand(Guid tenantId, string name) : base(tenantId) + { + Name = name; + } + + public override bool IsValid() + { + ValidationResult = s_validation.Validate(this); + return ValidationResult.IsValid; + } +} \ No newline at end of file diff --git a/CleanArchitecture.Domain/Commands/Tenants/UpdateTenant/UpdateTenantCommandHandler.cs b/CleanArchitecture.Domain/Commands/Tenants/UpdateTenant/UpdateTenantCommandHandler.cs new file mode 100644 index 0000000..f1ba406 --- /dev/null +++ b/CleanArchitecture.Domain/Commands/Tenants/UpdateTenant/UpdateTenantCommandHandler.cs @@ -0,0 +1,55 @@ +using System.Threading; +using System.Threading.Tasks; +using CleanArchitecture.Domain.Errors; +using CleanArchitecture.Domain.Events.Tenant; +using CleanArchitecture.Domain.Interfaces; +using CleanArchitecture.Domain.Interfaces.Repositories; +using CleanArchitecture.Domain.Notifications; +using MediatR; + +namespace CleanArchitecture.Domain.Commands.Tenants.UpdateTenant; + +public sealed class UpdateTenantCommandHandler : CommandHandlerBase, + IRequestHandler +{ + private readonly ITenantRepository _tenantRepository; + + public UpdateTenantCommandHandler( + IMediatorHandler bus, + IUnitOfWork unitOfWork, + INotificationHandler notifications, + ITenantRepository tenantRepository) : base(bus, unitOfWork, notifications) + { + _tenantRepository = tenantRepository; + } + + public async Task Handle(UpdateTenantCommand request, CancellationToken cancellationToken) + { + if (!await TestValidityAsync(request)) + { + return; + } + + var tenant = await _tenantRepository.GetByIdAsync(request.AggregateId); + + if (tenant is null) + { + await NotifyAsync( + new DomainNotification( + request.MessageType, + $"There is no tenant with Id {request.AggregateId}", + ErrorCodes.ObjectNotFound)); + + return; + } + + tenant.SetName(request.Name); + + if (await CommitAsync()) + { + await Bus.RaiseEventAsync(new TenantUpdatedEvent( + tenant.Id, + tenant.Name)); + } + } +} \ No newline at end of file diff --git a/CleanArchitecture.Domain/Commands/Tenants/UpdateTenant/UpdateTenantCommandValidation.cs b/CleanArchitecture.Domain/Commands/Tenants/UpdateTenant/UpdateTenantCommandValidation.cs new file mode 100644 index 0000000..905e638 --- /dev/null +++ b/CleanArchitecture.Domain/Commands/Tenants/UpdateTenant/UpdateTenantCommandValidation.cs @@ -0,0 +1,33 @@ +using CleanArchitecture.Domain.Constants; +using CleanArchitecture.Domain.Errors; +using FluentValidation; + +namespace CleanArchitecture.Domain.Commands.Tenants.UpdateTenant; + +public sealed class UpdateTenantCommandValidation : AbstractValidator +{ + public UpdateTenantCommandValidation() + { + AddRuleForId(); + AddRuleForName(); + } + + private void AddRuleForId() + { + RuleFor(cmd => cmd.AggregateId) + .NotEmpty() + .WithErrorCode(DomainErrorCodes.Tenant.TenantEmptyId) + .WithMessage("Tenant id may not be empty"); + } + + private void AddRuleForName() + { + RuleFor(cmd => cmd.Name) + .NotEmpty() + .WithErrorCode(DomainErrorCodes.Tenant.TenantEmptyName) + .WithMessage("Name may not be empty") + .MaximumLength(MaxLengths.Tenant.Name) + .WithErrorCode(DomainErrorCodes.Tenant.TenantNameExceedsMaxLength) + .WithMessage($"Name may not be longer than {MaxLengths.Tenant.Name} characters"); + } +} \ No newline at end of file diff --git a/CleanArchitecture.Domain/Commands/Users/ChangePassword/ChangePasswordCommand.cs b/CleanArchitecture.Domain/Commands/Users/ChangePassword/ChangePasswordCommand.cs index 4a27534..8d60484 100644 --- a/CleanArchitecture.Domain/Commands/Users/ChangePassword/ChangePasswordCommand.cs +++ b/CleanArchitecture.Domain/Commands/Users/ChangePassword/ChangePasswordCommand.cs @@ -4,7 +4,7 @@ namespace CleanArchitecture.Domain.Commands.Users.ChangePassword; public sealed class ChangePasswordCommand : CommandBase { - private readonly ChangePasswordCommandValidation _validation = new(); + private static readonly ChangePasswordCommandValidation s_validation = new(); public ChangePasswordCommand(string password, string newPassword) : base(Guid.NewGuid()) { @@ -17,7 +17,7 @@ public sealed class ChangePasswordCommand : CommandBase public override bool IsValid() { - ValidationResult = _validation.Validate(this); + ValidationResult = s_validation.Validate(this); return ValidationResult.IsValid; } } \ No newline at end of file diff --git a/CleanArchitecture.Domain/Commands/Users/ChangePassword/ChangePasswordCommandHandler.cs b/CleanArchitecture.Domain/Commands/Users/ChangePassword/ChangePasswordCommandHandler.cs index 38a85dd..713d43e 100644 --- a/CleanArchitecture.Domain/Commands/Users/ChangePassword/ChangePasswordCommandHandler.cs +++ b/CleanArchitecture.Domain/Commands/Users/ChangePassword/ChangePasswordCommandHandler.cs @@ -36,12 +36,12 @@ public sealed class ChangePasswordCommandHandler : CommandHandlerBase, var user = await _userRepository.GetByIdAsync(_user.GetUserId()); - if (user == null) + if (user is null) { await NotifyAsync( new DomainNotification( request.MessageType, - $"There is no User with Id {_user.GetUserId()}", + $"There is no user with Id {_user.GetUserId()}", ErrorCodes.ObjectNotFound)); return; @@ -53,7 +53,7 @@ public sealed class ChangePasswordCommandHandler : CommandHandlerBase, new DomainNotification( request.MessageType, "The password is incorrect", - DomainErrorCodes.UserPasswordIncorrect)); + DomainErrorCodes.User.UserPasswordIncorrect)); return; } diff --git a/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommand.cs b/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommand.cs index 62406ad..fb17c86 100644 --- a/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommand.cs +++ b/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommand.cs @@ -4,7 +4,7 @@ namespace CleanArchitecture.Domain.Commands.Users.CreateUser; public sealed class CreateUserCommand : CommandBase { - private readonly CreateUserCommandValidation _validation = new(); + private static readonly CreateUserCommandValidation s_validation = new(); public CreateUserCommand( Guid userId, @@ -31,7 +31,7 @@ public sealed class CreateUserCommand : CommandBase public override bool IsValid() { - ValidationResult = _validation.Validate(this); + ValidationResult = s_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 index 464aad4..7e40c4d 100644 --- a/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommandHandler.cs +++ b/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommandHandler.cs @@ -35,25 +35,25 @@ public sealed class CreateUserCommandHandler : CommandHandlerBase, var existingUser = await _userRepository.GetByIdAsync(request.UserId); - if (existingUser != null) + if (existingUser is not null) { await Bus.RaiseEventAsync( new DomainNotification( request.MessageType, - $"There is already a User with Id {request.UserId}", - DomainErrorCodes.UserAlreadyExists)); + $"There is already a user with Id {request.UserId}", + DomainErrorCodes.User.UserAlreadyExists)); return; } existingUser = await _userRepository.GetByEmailAsync(request.Email); - if (existingUser != null) + if (existingUser is not null) { await Bus.RaiseEventAsync( new DomainNotification( request.MessageType, - $"There is already a User with Email {request.Email}", - DomainErrorCodes.UserAlreadyExists)); + $"There is already a user with email {request.Email}", + DomainErrorCodes.User.UserAlreadyExists)); return; } diff --git a/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommandValidation.cs b/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommandValidation.cs index 5a51947..c2d9564 100644 --- a/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommandValidation.cs +++ b/CleanArchitecture.Domain/Commands/Users/CreateUser/CreateUserCommandValidation.cs @@ -20,7 +20,7 @@ public sealed class CreateUserCommandValidation : AbstractValidator cmd.UserId) .NotEmpty() - .WithErrorCode(DomainErrorCodes.UserEmptyId) + .WithErrorCode(DomainErrorCodes.User.UserEmptyId) .WithMessage("User id may not be empty"); } @@ -28,10 +28,10 @@ public sealed class CreateUserCommandValidation : AbstractValidator cmd.Email) .EmailAddress() - .WithErrorCode(DomainErrorCodes.UserInvalidEmail) + .WithErrorCode(DomainErrorCodes.User.UserInvalidEmail) .WithMessage("Email is not a valid email address") .MaximumLength(MaxLengths.User.Email) - .WithErrorCode(DomainErrorCodes.UserEmailExceedsMaxLength) + .WithErrorCode(DomainErrorCodes.User.UserEmailExceedsMaxLength) .WithMessage("Email may not be longer than 320 characters"); } @@ -39,10 +39,10 @@ public sealed class CreateUserCommandValidation : AbstractValidator cmd.FirstName) .NotEmpty() - .WithErrorCode(DomainErrorCodes.UserEmptyFirstName) + .WithErrorCode(DomainErrorCodes.User.UserEmptyFirstName) .WithMessage("FirstName may not be empty") .MaximumLength(MaxLengths.User.FirstName) - .WithErrorCode(DomainErrorCodes.UserFirstNameExceedsMaxLength) + .WithErrorCode(DomainErrorCodes.User.UserFirstNameExceedsMaxLength) .WithMessage("FirstName may not be longer than 100 characters"); } @@ -50,10 +50,10 @@ public sealed class CreateUserCommandValidation : AbstractValidator cmd.LastName) .NotEmpty() - .WithErrorCode(DomainErrorCodes.UserEmptyLastName) + .WithErrorCode(DomainErrorCodes.User.UserEmptyLastName) .WithMessage("LastName may not be empty") .MaximumLength(MaxLengths.User.LastName) - .WithErrorCode(DomainErrorCodes.UserLastNameExceedsMaxLength) + .WithErrorCode(DomainErrorCodes.User.UserLastNameExceedsMaxLength) .WithMessage("LastName may not be longer than 100 characters"); } diff --git a/CleanArchitecture.Domain/Commands/Users/DeleteUser/DeleteUserCommand.cs b/CleanArchitecture.Domain/Commands/Users/DeleteUser/DeleteUserCommand.cs index 48c42f5..2aa4910 100644 --- a/CleanArchitecture.Domain/Commands/Users/DeleteUser/DeleteUserCommand.cs +++ b/CleanArchitecture.Domain/Commands/Users/DeleteUser/DeleteUserCommand.cs @@ -4,7 +4,7 @@ namespace CleanArchitecture.Domain.Commands.Users.DeleteUser; public sealed class DeleteUserCommand : CommandBase { - private readonly DeleteUserCommandValidation _validation = new(); + private static readonly DeleteUserCommandValidation s_validation = new(); public DeleteUserCommand(Guid userId) : base(userId) { @@ -15,7 +15,7 @@ public sealed class DeleteUserCommand : CommandBase public override bool IsValid() { - ValidationResult = _validation.Validate(this); + ValidationResult = s_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 index 1fc4970..e7d69b5 100644 --- a/CleanArchitecture.Domain/Commands/Users/DeleteUser/DeleteUserCommandHandler.cs +++ b/CleanArchitecture.Domain/Commands/Users/DeleteUser/DeleteUserCommandHandler.cs @@ -36,12 +36,12 @@ public sealed class DeleteUserCommandHandler : CommandHandlerBase, var user = await _userRepository.GetByIdAsync(request.UserId); - if (user == null) + if (user is null) { await NotifyAsync( new DomainNotification( request.MessageType, - $"There is no User with Id {request.UserId}", + $"There is no user with Id {request.UserId}", ErrorCodes.ObjectNotFound)); return; diff --git a/CleanArchitecture.Domain/Commands/Users/DeleteUser/DeleteUserCommandValidation.cs b/CleanArchitecture.Domain/Commands/Users/DeleteUser/DeleteUserCommandValidation.cs index 99dc44a..7ca78ad 100644 --- a/CleanArchitecture.Domain/Commands/Users/DeleteUser/DeleteUserCommandValidation.cs +++ b/CleanArchitecture.Domain/Commands/Users/DeleteUser/DeleteUserCommandValidation.cs @@ -14,7 +14,7 @@ public sealed class DeleteUserCommandValidation : AbstractValidator cmd.UserId) .NotEmpty() - .WithErrorCode(DomainErrorCodes.UserEmptyId) + .WithErrorCode(DomainErrorCodes.User.UserEmptyId) .WithMessage("User id may not be empty"); } } \ No newline at end of file diff --git a/CleanArchitecture.Domain/Commands/Users/LoginUser/LoginUserCommand.cs b/CleanArchitecture.Domain/Commands/Users/LoginUser/LoginUserCommand.cs index 2fcdc00..d5bc6e6 100644 --- a/CleanArchitecture.Domain/Commands/Users/LoginUser/LoginUserCommand.cs +++ b/CleanArchitecture.Domain/Commands/Users/LoginUser/LoginUserCommand.cs @@ -6,7 +6,7 @@ namespace CleanArchitecture.Domain.Commands.Users.LoginUser; public sealed class LoginUserCommand : CommandBase, IRequest { - private readonly LoginUserCommandValidation _validation = new(); + private static readonly LoginUserCommandValidation s_validation = new(); public LoginUserCommand( @@ -22,7 +22,7 @@ public sealed class LoginUserCommand : CommandBase, public override bool IsValid() { - ValidationResult = _validation.Validate(this); + ValidationResult = s_validation.Validate(this); return ValidationResult.IsValid; } } \ No newline at end of file diff --git a/CleanArchitecture.Domain/Commands/Users/LoginUser/LoginUserCommandHandler.cs b/CleanArchitecture.Domain/Commands/Users/LoginUser/LoginUserCommandHandler.cs index dee2b53..aee5fe6 100644 --- a/CleanArchitecture.Domain/Commands/Users/LoginUser/LoginUserCommandHandler.cs +++ b/CleanArchitecture.Domain/Commands/Users/LoginUser/LoginUserCommandHandler.cs @@ -45,12 +45,12 @@ public sealed class LoginUserCommandHandler : CommandHandlerBase, var user = await _userRepository.GetByEmailAsync(request.Email); - if (user == null) + if (user is null) { await NotifyAsync( new DomainNotification( request.MessageType, - $"There is no User with Email {request.Email}", + $"There is no user with email {request.Email}", ErrorCodes.ObjectNotFound)); return ""; @@ -64,7 +64,7 @@ public sealed class LoginUserCommandHandler : CommandHandlerBase, new DomainNotification( request.MessageType, "The password is incorrect", - DomainErrorCodes.UserPasswordIncorrect)); + DomainErrorCodes.User.UserPasswordIncorrect)); return ""; } diff --git a/CleanArchitecture.Domain/Commands/Users/LoginUser/LoginUserCommandValidation.cs b/CleanArchitecture.Domain/Commands/Users/LoginUser/LoginUserCommandValidation.cs index 3be735b..8148946 100644 --- a/CleanArchitecture.Domain/Commands/Users/LoginUser/LoginUserCommandValidation.cs +++ b/CleanArchitecture.Domain/Commands/Users/LoginUser/LoginUserCommandValidation.cs @@ -17,10 +17,10 @@ public sealed class LoginUserCommandValidation : AbstractValidator cmd.Email) .EmailAddress() - .WithErrorCode(DomainErrorCodes.UserInvalidEmail) + .WithErrorCode(DomainErrorCodes.User.UserInvalidEmail) .WithMessage("Email is not a valid email address") .MaximumLength(MaxLengths.User.Email) - .WithErrorCode(DomainErrorCodes.UserEmailExceedsMaxLength) + .WithErrorCode(DomainErrorCodes.User.UserEmailExceedsMaxLength) .WithMessage("Email may not be longer than 320 characters"); } diff --git a/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommand.cs b/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommand.cs index 13bc3d9..d91434d 100644 --- a/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommand.cs +++ b/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommand.cs @@ -5,7 +5,7 @@ namespace CleanArchitecture.Domain.Commands.Users.UpdateUser; public sealed class UpdateUserCommand : CommandBase { - private readonly UpdateUserCommandValidation _validation = new(); + private static readonly UpdateUserCommandValidation s_validation = new(); public UpdateUserCommand( Guid userId, @@ -29,7 +29,7 @@ public sealed class UpdateUserCommand : CommandBase public override bool IsValid() { - ValidationResult = _validation.Validate(this); + ValidationResult = s_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 index cf0227e..93c7e1a 100644 --- a/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommandHandler.cs +++ b/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommandHandler.cs @@ -36,12 +36,12 @@ public sealed class UpdateUserCommandHandler : CommandHandlerBase, var user = await _userRepository.GetByIdAsync(request.UserId); - if (user == null) + if (user is null) { await Bus.RaiseEventAsync( new DomainNotification( request.MessageType, - $"There is no User with Id {request.UserId}", + $"There is no user with Id {request.UserId}", ErrorCodes.ObjectNotFound)); return; } @@ -61,13 +61,13 @@ public sealed class UpdateUserCommandHandler : CommandHandlerBase, { var existingUser = await _userRepository.GetByEmailAsync(request.Email); - if (existingUser != null) + if (existingUser is not null) { await Bus.RaiseEventAsync( new DomainNotification( request.MessageType, - $"There is already a User with Email {request.Email}", - DomainErrorCodes.UserAlreadyExists)); + $"There is already a user with email {request.Email}", + DomainErrorCodes.User.UserAlreadyExists)); return; } } diff --git a/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommandValidation.cs b/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommandValidation.cs index 17abee3..dbbb4f4 100644 --- a/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommandValidation.cs +++ b/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommandValidation.cs @@ -19,7 +19,7 @@ public sealed class UpdateUserCommandValidation : AbstractValidator cmd.UserId) .NotEmpty() - .WithErrorCode(DomainErrorCodes.UserEmptyId) + .WithErrorCode(DomainErrorCodes.User.UserEmptyId) .WithMessage("User id may not be empty"); } @@ -27,10 +27,10 @@ public sealed class UpdateUserCommandValidation : AbstractValidator cmd.Email) .EmailAddress() - .WithErrorCode(DomainErrorCodes.UserInvalidEmail) + .WithErrorCode(DomainErrorCodes.User.UserInvalidEmail) .WithMessage("Email is not a valid email address") .MaximumLength(MaxLengths.User.Email) - .WithErrorCode(DomainErrorCodes.UserEmailExceedsMaxLength) + .WithErrorCode(DomainErrorCodes.User.UserEmailExceedsMaxLength) .WithMessage("Email may not be longer than 320 characters"); } @@ -38,10 +38,10 @@ public sealed class UpdateUserCommandValidation : AbstractValidator cmd.FirstName) .NotEmpty() - .WithErrorCode(DomainErrorCodes.UserEmptyFirstName) + .WithErrorCode(DomainErrorCodes.User.UserEmptyFirstName) .WithMessage("FirstName may not be empty") .MaximumLength(MaxLengths.User.FirstName) - .WithErrorCode(DomainErrorCodes.UserFirstNameExceedsMaxLength) + .WithErrorCode(DomainErrorCodes.User.UserFirstNameExceedsMaxLength) .WithMessage("FirstName may not be longer than 100 characters"); } @@ -49,10 +49,10 @@ public sealed class UpdateUserCommandValidation : AbstractValidator cmd.LastName) .NotEmpty() - .WithErrorCode(DomainErrorCodes.UserEmptyLastName) + .WithErrorCode(DomainErrorCodes.User.UserEmptyLastName) .WithMessage("LastName may not be empty") .MaximumLength(MaxLengths.User.LastName) - .WithErrorCode(DomainErrorCodes.UserLastNameExceedsMaxLength) + .WithErrorCode(DomainErrorCodes.User.UserLastNameExceedsMaxLength) .WithMessage("LastName may not be longer than 100 characters"); } @@ -60,7 +60,7 @@ public sealed class UpdateUserCommandValidation : AbstractValidator cmd.Role) .IsInEnum() - .WithErrorCode(DomainErrorCodes.UserInvalidRole) + .WithErrorCode(DomainErrorCodes.User.UserInvalidRole) .WithMessage("Role is not a valid role"); } } \ No newline at end of file diff --git a/CleanArchitecture.Domain/Entities/Tenant.cs b/CleanArchitecture.Domain/Entities/Tenant.cs index 555b095..39cefb6 100644 --- a/CleanArchitecture.Domain/Entities/Tenant.cs +++ b/CleanArchitecture.Domain/Entities/Tenant.cs @@ -15,4 +15,9 @@ public class Tenant : Entity { Name = name; } + + public void SetName(string name) + { + Name = name; + } } \ No newline at end of file diff --git a/CleanArchitecture.Domain/Errors/DomainErrorCodes.cs b/CleanArchitecture.Domain/Errors/DomainErrorCodes.cs index a6091de..d4131c8 100644 --- a/CleanArchitecture.Domain/Errors/DomainErrorCodes.cs +++ b/CleanArchitecture.Domain/Errors/DomainErrorCodes.cs @@ -2,26 +2,40 @@ namespace CleanArchitecture.Domain.Errors; public static class DomainErrorCodes { - // User Validation - public const string UserEmptyId = "USER_EMPTY_ID"; - public const string UserEmptyFirstName = "USER_EMPTY_FIRST_NAME"; - public const string UserEmptyLastName = "USER_EMPTY_LAST_NAME"; - public const string UserEmailExceedsMaxLength = "USER_EMAIL_EXCEEDS_MAX_LENGTH"; - public const string UserFirstNameExceedsMaxLength = "USER_FIRST_NAME_EXCEEDS_MAX_LENGTH"; - public const string UserLastNameExceedsMaxLength = "USER_LAST_NAME_EXCEEDS_MAX_LENGTH"; - public const string UserInvalidEmail = "USER_INVALID_EMAIL"; - public const string UserInvalidRole = "USER_INVALID_ROLE"; + public static class User + { + // User Validation + public const string UserEmptyId = "USER_EMPTY_ID"; + public const string UserEmptyFirstName = "USER_EMPTY_FIRST_NAME"; + public const string UserEmptyLastName = "USER_EMPTY_LAST_NAME"; + public const string UserEmailExceedsMaxLength = "USER_EMAIL_EXCEEDS_MAX_LENGTH"; + public const string UserFirstNameExceedsMaxLength = "USER_FIRST_NAME_EXCEEDS_MAX_LENGTH"; + public const string UserLastNameExceedsMaxLength = "USER_LAST_NAME_EXCEEDS_MAX_LENGTH"; + public const string UserInvalidEmail = "USER_INVALID_EMAIL"; + public const string UserInvalidRole = "USER_INVALID_ROLE"; - // User Password Validation - public const string UserEmptyPassword = "USER_PASSWORD_MAY_NOT_BE_EMPTY"; - public const string UserShortPassword = "USER_PASSWORD_MAY_NOT_BE_SHORTER_THAN_6_CHARACTERS"; - public const string UserLongPassword = "USER_PASSWORD_MAY_NOT_BE_LONGER_THAN_50_CHARACTERS"; - public const string UserUppercaseLetterPassword = "USER_PASSWORD_MUST_CONTAIN_A_UPPERCASE_LETTER"; - public const string UserLowercaseLetterPassword = "USER_PASSWORD_MUST_CONTAIN_A_LOWERCASE_LETTER"; - public const string UserNumberPassword = "USER_PASSWORD_MUST_CONTAIN_A_NUMBER"; - public const string UserSpecialCharPassword = "USER_PASSWORD_MUST_CONTAIN_A_SPECIAL_CHARACTER"; + // User Password Validation + public const string UserEmptyPassword = "USER_PASSWORD_MAY_NOT_BE_EMPTY"; + public const string UserShortPassword = "USER_PASSWORD_MAY_NOT_BE_SHORTER_THAN_6_CHARACTERS"; + public const string UserLongPassword = "USER_PASSWORD_MAY_NOT_BE_LONGER_THAN_50_CHARACTERS"; + public const string UserUppercaseLetterPassword = "USER_PASSWORD_MUST_CONTAIN_A_UPPERCASE_LETTER"; + public const string UserLowercaseLetterPassword = "USER_PASSWORD_MUST_CONTAIN_A_LOWERCASE_LETTER"; + public const string UserNumberPassword = "USER_PASSWORD_MUST_CONTAIN_A_NUMBER"; + public const string UserSpecialCharPassword = "USER_PASSWORD_MUST_CONTAIN_A_SPECIAL_CHARACTER"; - // User - public const string UserAlreadyExists = "USER_ALREADY_EXISTS"; - public const string UserPasswordIncorrect = "USER_PASSWORD_INCORRECT"; + // General + public const string UserAlreadyExists = "USER_ALREADY_EXISTS"; + public const string UserPasswordIncorrect = "USER_PASSWORD_INCORRECT"; + } + + public static class Tenant + { + // Tenant Validation + public const string TenantEmptyId = "TENANT_EMPTY_ID"; + public const string TenantEmptyName = "TENANT_EMPTY_NAME"; + public const string TenantNameExceedsMaxLength = "TENANT_NAME_EXCEEDS_MAX_LENGTH"; + + // General + public const string TenantAlreadyExists = "TENANT_ALREADY_EXISTS"; + } } \ No newline at end of file diff --git a/CleanArchitecture.Domain/EventHandler/TenantEventHandler.cs b/CleanArchitecture.Domain/EventHandler/TenantEventHandler.cs new file mode 100644 index 0000000..ed46c1a --- /dev/null +++ b/CleanArchitecture.Domain/EventHandler/TenantEventHandler.cs @@ -0,0 +1,27 @@ +using System.Threading; +using System.Threading.Tasks; +using CleanArchitecture.Domain.Events.Tenant; +using MediatR; + +namespace CleanArchitecture.Domain.EventHandler; + +public sealed class TenantEventHandler : + INotificationHandler, + INotificationHandler, + INotificationHandler +{ + public Task Handle(TenantCreatedEvent notification, CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + + public Task Handle(TenantDeletedEvent notification, CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + + public Task Handle(TenantUpdatedEvent notification, CancellationToken cancellationToken) + { + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/CleanArchitecture.Domain/Events/Tenant/TenantCreatedEvent.cs b/CleanArchitecture.Domain/Events/Tenant/TenantCreatedEvent.cs new file mode 100644 index 0000000..b79f34e --- /dev/null +++ b/CleanArchitecture.Domain/Events/Tenant/TenantCreatedEvent.cs @@ -0,0 +1,14 @@ +using System; +using CleanArchitecture.Domain.DomainEvents; + +namespace CleanArchitecture.Domain.Events.Tenant; + +public sealed class TenantCreatedEvent : DomainEvent +{ + public string Name { get; set; } + + public TenantCreatedEvent(Guid tenantId, string name) : base(tenantId) + { + Name = name; + } +} \ No newline at end of file diff --git a/CleanArchitecture.Domain/Events/Tenant/TenantDeletedEvent.cs b/CleanArchitecture.Domain/Events/Tenant/TenantDeletedEvent.cs new file mode 100644 index 0000000..e96bdfd --- /dev/null +++ b/CleanArchitecture.Domain/Events/Tenant/TenantDeletedEvent.cs @@ -0,0 +1,11 @@ +using System; +using CleanArchitecture.Domain.DomainEvents; + +namespace CleanArchitecture.Domain.Events.Tenant; + +public sealed class TenantDeletedEvent : DomainEvent +{ + public TenantDeletedEvent(Guid tenantId) : base(tenantId) + { + } +} \ No newline at end of file diff --git a/CleanArchitecture.Domain/Events/Tenant/TenantUpdatedEvent.cs b/CleanArchitecture.Domain/Events/Tenant/TenantUpdatedEvent.cs new file mode 100644 index 0000000..00b66b3 --- /dev/null +++ b/CleanArchitecture.Domain/Events/Tenant/TenantUpdatedEvent.cs @@ -0,0 +1,14 @@ +using System; +using CleanArchitecture.Domain.DomainEvents; + +namespace CleanArchitecture.Domain.Events.Tenant; + +public sealed class TenantUpdatedEvent : DomainEvent +{ + public string Name { get; set; } + + public TenantUpdatedEvent(Guid tenantId, string name) : base(tenantId) + { + Name = name; + } +} \ No newline at end of file diff --git a/CleanArchitecture.Domain/Events/User/PasswordChangedEvent.cs b/CleanArchitecture.Domain/Events/User/PasswordChangedEvent.cs index e3bf3f1..c5b1b25 100644 --- a/CleanArchitecture.Domain/Events/User/PasswordChangedEvent.cs +++ b/CleanArchitecture.Domain/Events/User/PasswordChangedEvent.cs @@ -7,8 +7,5 @@ public sealed class PasswordChangedEvent : DomainEvent { public PasswordChangedEvent(Guid userId) : base(userId) { - UserId = userId; } - - public Guid UserId { get; } } \ No newline at end of file diff --git a/CleanArchitecture.Domain/Events/User/UserCreatedEvent.cs b/CleanArchitecture.Domain/Events/User/UserCreatedEvent.cs index f21e681..95d11bf 100644 --- a/CleanArchitecture.Domain/Events/User/UserCreatedEvent.cs +++ b/CleanArchitecture.Domain/Events/User/UserCreatedEvent.cs @@ -7,8 +7,5 @@ public sealed class UserCreatedEvent : DomainEvent { public UserCreatedEvent(Guid userId) : base(userId) { - UserId = userId; } - - public Guid UserId { get; } } \ No newline at end of file diff --git a/CleanArchitecture.Domain/Events/User/UserDeletedEvent.cs b/CleanArchitecture.Domain/Events/User/UserDeletedEvent.cs index 5245879..8b485f5 100644 --- a/CleanArchitecture.Domain/Events/User/UserDeletedEvent.cs +++ b/CleanArchitecture.Domain/Events/User/UserDeletedEvent.cs @@ -7,8 +7,5 @@ public sealed class UserDeletedEvent : DomainEvent { public UserDeletedEvent(Guid userId) : base(userId) { - UserId = userId; } - - public Guid UserId { get; } } \ No newline at end of file diff --git a/CleanArchitecture.Domain/Events/User/UserUpdatedEvent.cs b/CleanArchitecture.Domain/Events/User/UserUpdatedEvent.cs index d78cd72..7056b95 100644 --- a/CleanArchitecture.Domain/Events/User/UserUpdatedEvent.cs +++ b/CleanArchitecture.Domain/Events/User/UserUpdatedEvent.cs @@ -7,8 +7,5 @@ public sealed class UserUpdatedEvent : DomainEvent { public UserUpdatedEvent(Guid userId) : base(userId) { - UserId = userId; } - - public Guid UserId { get; } } \ No newline at end of file diff --git a/CleanArchitecture.Domain/Extensions/ServiceCollectionExtension.cs b/CleanArchitecture.Domain/Extensions/ServiceCollectionExtension.cs index 4c3b2b9..a82ac0a 100644 --- a/CleanArchitecture.Domain/Extensions/ServiceCollectionExtension.cs +++ b/CleanArchitecture.Domain/Extensions/ServiceCollectionExtension.cs @@ -1,9 +1,13 @@ +using CleanArchitecture.Domain.Commands.Tenants.CreateTenant; +using CleanArchitecture.Domain.Commands.Tenants.DeleteTenant; +using CleanArchitecture.Domain.Commands.Tenants.UpdateTenant; using CleanArchitecture.Domain.Commands.Users.ChangePassword; using CleanArchitecture.Domain.Commands.Users.CreateUser; using CleanArchitecture.Domain.Commands.Users.DeleteUser; using CleanArchitecture.Domain.Commands.Users.LoginUser; using CleanArchitecture.Domain.Commands.Users.UpdateUser; using CleanArchitecture.Domain.EventHandler; +using CleanArchitecture.Domain.Events.Tenant; using CleanArchitecture.Domain.Events.User; using CleanArchitecture.Domain.Interfaces; using MediatR; @@ -22,6 +26,10 @@ public static class ServiceCollectionExtension services.AddScoped, ChangePasswordCommandHandler>(); services.AddScoped, LoginUserCommandHandler>(); + // Tenant + services.AddScoped, CreateTenantCommandHandler>(); + services.AddScoped, UpdateTenantCommandHandler>(); + services.AddScoped, DeleteTenantCommandHandler>(); return services; } @@ -33,6 +41,11 @@ public static class ServiceCollectionExtension services.AddScoped, UserEventHandler>(); services.AddScoped, UserEventHandler>(); services.AddScoped, UserEventHandler>(); + + // Tenant + services.AddScoped, TenantEventHandler>(); + services.AddScoped, TenantEventHandler>(); + services.AddScoped, TenantEventHandler>(); return services; } diff --git a/CleanArchitecture.Domain/Extensions/Validation/CustomValidator.cs b/CleanArchitecture.Domain/Extensions/Validation/CustomValidator.cs index 5157f16..dea14c7 100644 --- a/CleanArchitecture.Domain/Extensions/Validation/CustomValidator.cs +++ b/CleanArchitecture.Domain/Extensions/Validation/CustomValidator.cs @@ -23,13 +23,13 @@ public static partial class CustomValidator int maxLength = 50) { var options = ruleBuilder - .NotEmpty().WithErrorCode(DomainErrorCodes.UserEmptyPassword) - .MinimumLength(minLength).WithErrorCode(DomainErrorCodes.UserShortPassword) - .MaximumLength(maxLength).WithErrorCode(DomainErrorCodes.UserLongPassword) - .Matches("[A-Z]").WithErrorCode(DomainErrorCodes.UserUppercaseLetterPassword) - .Matches("[a-z]").WithErrorCode(DomainErrorCodes.UserLowercaseLetterPassword) - .Matches("[0-9]").WithErrorCode(DomainErrorCodes.UserNumberPassword) - .Matches("[^a-zA-Z0-9]").WithErrorCode(DomainErrorCodes.UserSpecialCharPassword); + .NotEmpty().WithErrorCode(DomainErrorCodes.User.UserEmptyPassword) + .MinimumLength(minLength).WithErrorCode(DomainErrorCodes.User.UserShortPassword) + .MaximumLength(maxLength).WithErrorCode(DomainErrorCodes.User.UserLongPassword) + .Matches("[A-Z]").WithErrorCode(DomainErrorCodes.User.UserUppercaseLetterPassword) + .Matches("[a-z]").WithErrorCode(DomainErrorCodes.User.UserLowercaseLetterPassword) + .Matches("[0-9]").WithErrorCode(DomainErrorCodes.User.UserNumberPassword) + .Matches("[^a-zA-Z0-9]").WithErrorCode(DomainErrorCodes.User.UserSpecialCharPassword); return options; } diff --git a/CleanArchitecture.Domain/Interfaces/Repositories/IRepository.cs b/CleanArchitecture.Domain/Interfaces/Repositories/IRepository.cs index 7c162bd..5d90b49 100644 --- a/CleanArchitecture.Domain/Interfaces/Repositories/IRepository.cs +++ b/CleanArchitecture.Domain/Interfaces/Repositories/IRepository.cs @@ -22,4 +22,5 @@ public interface IRepository : IDisposable where TEntity : Entity Task ExistsAsync(Guid id); public void Remove(TEntity entity, bool hardDelete = false); + void RemoveRange(IEnumerable entities, bool hardDelete = false); } \ No newline at end of file diff --git a/CleanArchitecture.Domain/Interfaces/Repositories/ITenantRepository.cs b/CleanArchitecture.Domain/Interfaces/Repositories/ITenantRepository.cs new file mode 100644 index 0000000..d2e0464 --- /dev/null +++ b/CleanArchitecture.Domain/Interfaces/Repositories/ITenantRepository.cs @@ -0,0 +1,7 @@ +using CleanArchitecture.Domain.Entities; + +namespace CleanArchitecture.Domain.Interfaces.Repositories; + +public interface ITenantRepository : IRepository +{ +} \ No newline at end of file diff --git a/CleanArchitecture.Infrastructure/EventSourcing/EventStoreContext.cs b/CleanArchitecture.Infrastructure/EventSourcing/EventStoreContext.cs index 0edaffa..b28104b 100644 --- a/CleanArchitecture.Infrastructure/EventSourcing/EventStoreContext.cs +++ b/CleanArchitecture.Infrastructure/EventSourcing/EventStoreContext.cs @@ -13,7 +13,7 @@ public sealed class EventStoreContext : IEventStoreContext { _user = user; - if (httpContextAccessor?.HttpContext == null || + if (httpContextAccessor?.HttpContext is null || !httpContextAccessor.HttpContext.Request.Headers.TryGetValue("X-CLEAN-ARCHITECTURE-CORRELATION-ID", out var id)) { _correlationId = $"internal - {Guid.NewGuid()}"; diff --git a/CleanArchitecture.Infrastructure/Extensions/ServiceCollectionExtensions.cs b/CleanArchitecture.Infrastructure/Extensions/ServiceCollectionExtensions.cs index 02d063f..aec00dd 100644 --- a/CleanArchitecture.Infrastructure/Extensions/ServiceCollectionExtensions.cs +++ b/CleanArchitecture.Infrastructure/Extensions/ServiceCollectionExtensions.cs @@ -46,6 +46,7 @@ public static class ServiceCollectionExtensions // Repositories services.AddScoped(); + services.AddScoped(); return services; } diff --git a/CleanArchitecture.Infrastructure/Repositories/BaseRepository.cs b/CleanArchitecture.Infrastructure/Repositories/BaseRepository.cs index db13d69..9137422 100644 --- a/CleanArchitecture.Infrastructure/Repositories/BaseRepository.cs +++ b/CleanArchitecture.Infrastructure/Repositories/BaseRepository.cs @@ -55,9 +55,9 @@ public class BaseRepository : IRepository where TEntity : Enti DbSet.Update(entity); } - public Task ExistsAsync(Guid id) + public async Task ExistsAsync(Guid id) { - return DbSet.AnyAsync(entity => entity.Id == id); + return await DbSet.AnyAsync(entity => entity.Id == id); } public void Remove(TEntity entity, bool hardDelete = false) @@ -72,6 +72,20 @@ public class BaseRepository : IRepository where TEntity : Enti DbSet.Update(entity); } } + + public void RemoveRange(IEnumerable entities, bool hardDelete = false) + { + if (hardDelete) + { + DbSet.RemoveRange(entities); + return; + } + + foreach (var entity in entities) + { + entity.Delete(); + } + } public int SaveChanges() { diff --git a/CleanArchitecture.Infrastructure/Repositories/TenantRepository.cs b/CleanArchitecture.Infrastructure/Repositories/TenantRepository.cs new file mode 100644 index 0000000..f86518b --- /dev/null +++ b/CleanArchitecture.Infrastructure/Repositories/TenantRepository.cs @@ -0,0 +1,12 @@ +using CleanArchitecture.Domain.Entities; +using CleanArchitecture.Domain.Interfaces.Repositories; +using CleanArchitecture.Infrastructure.Database; + +namespace CleanArchitecture.Infrastructure.Repositories; + +public sealed class TenantRepository : BaseRepository, ITenantRepository +{ + public TenantRepository(ApplicationDbContext context) : base(context) + { + } +} \ No newline at end of file diff --git a/CleanArchitecture.IntegrationTests/Extensions/FunctionalTestsServiceCollectionExtensions.cs b/CleanArchitecture.IntegrationTests/Extensions/FunctionalTestsServiceCollectionExtensions.cs index d81e2c0..f3c9b08 100644 --- a/CleanArchitecture.IntegrationTests/Extensions/FunctionalTestsServiceCollectionExtensions.cs +++ b/CleanArchitecture.IntegrationTests/Extensions/FunctionalTestsServiceCollectionExtensions.cs @@ -15,7 +15,7 @@ public static class FunctionalTestsServiceCollectionExtensions DbConnection connection) where TContext : DbContext { var descriptor = services.SingleOrDefault(d => d.ServiceType == typeof(DbContextOptions)); - if (descriptor != null) + if (descriptor is not null) services.Remove(descriptor); services.AddScoped(p => diff --git a/CleanArchitecture.IntegrationTests/gRPC/GetUsersByIdsTests.cs b/CleanArchitecture.IntegrationTests/gRPC/GetUsersByIdsTests.cs index 0ad9f75..8464d7d 100644 --- a/CleanArchitecture.IntegrationTests/gRPC/GetUsersByIdsTests.cs +++ b/CleanArchitecture.IntegrationTests/gRPC/GetUsersByIdsTests.cs @@ -21,7 +21,7 @@ public sealed class GetUsersByIdsTests : IClassFixture { var client = new UsersApi.UsersApiClient(_fixture.GrpcChannel); - var request = new GetByIdsRequest(); + var request = new GetUsersByIdsRequest(); request.Ids.Add(_fixture.CreatedUserId.ToString()); var response = await client.GetByIdsAsync(request); diff --git a/CleanArchitecture.Proto/CleanArchitecture.Proto.csproj b/CleanArchitecture.Proto/CleanArchitecture.Proto.csproj index df3c40c..1cef0c5 100644 --- a/CleanArchitecture.Proto/CleanArchitecture.Proto.csproj +++ b/CleanArchitecture.Proto/CleanArchitecture.Proto.csproj @@ -5,14 +5,11 @@ enable - - - - - + + diff --git a/CleanArchitecture.Proto/Tenants/Models.proto b/CleanArchitecture.Proto/Tenants/Models.proto new file mode 100644 index 0000000..c98c121 --- /dev/null +++ b/CleanArchitecture.Proto/Tenants/Models.proto @@ -0,0 +1,17 @@ +syntax = "proto3"; + +option csharp_namespace = "CleanArchitecture.Proto.Tenants"; + +message Tenant { + string id = 1; + string name = 2; + bool isDeleted = 3; +} + +message GetTenantsByIdsResult { + repeated Tenant tenants = 1; +} + +message GetTenantsByIdsRequest { + repeated string ids = 1; +} \ No newline at end of file diff --git a/CleanArchitecture.Proto/Tenants/TenantsApi.proto b/CleanArchitecture.Proto/Tenants/TenantsApi.proto new file mode 100644 index 0000000..776d2d7 --- /dev/null +++ b/CleanArchitecture.Proto/Tenants/TenantsApi.proto @@ -0,0 +1,9 @@ +syntax = "proto3"; + +option csharp_namespace = "CleanArchitecture.Proto.Tenants"; + +import "Tenants/Models.proto"; + +service TenantsApi { + rpc GetByIds(GetTenantsByIdsRequest) returns (GetTenantsByIdsResult); +} diff --git a/CleanArchitecture.Proto/Users/Models.proto b/CleanArchitecture.Proto/Users/Models.proto index 03e59a7..c372420 100644 --- a/CleanArchitecture.Proto/Users/Models.proto +++ b/CleanArchitecture.Proto/Users/Models.proto @@ -10,10 +10,10 @@ message GrpcUser { bool isDeleted = 6; } -message GetByIdsResult { +message GetUsersByIdsResult { repeated GrpcUser users = 1; } -message GetByIdsRequest { +message GetUsersByIdsRequest { repeated string ids = 1; } \ No newline at end of file diff --git a/CleanArchitecture.Proto/Users/UsersApi.proto b/CleanArchitecture.Proto/Users/UsersApi.proto index e718218..069393e 100644 --- a/CleanArchitecture.Proto/Users/UsersApi.proto +++ b/CleanArchitecture.Proto/Users/UsersApi.proto @@ -5,5 +5,5 @@ option csharp_namespace = "CleanArchitecture.Proto.Users"; import "Users/Models.proto"; service UsersApi { - rpc GetByIds(GetByIdsRequest) returns (GetByIdsResult); + rpc GetByIds(GetUsersByIdsRequest) returns (GetUsersByIdsResult); } diff --git a/CleanArchitecture.Shared/CleanArchitecture.Shared.csproj b/CleanArchitecture.Shared/CleanArchitecture.Shared.csproj index de3c61c..882f5b9 100644 --- a/CleanArchitecture.Shared/CleanArchitecture.Shared.csproj +++ b/CleanArchitecture.Shared/CleanArchitecture.Shared.csproj @@ -5,4 +5,5 @@ enable + diff --git a/CleanArchitecture.Shared/Tenants/TenantViewModel.cs b/CleanArchitecture.Shared/Tenants/TenantViewModel.cs new file mode 100644 index 0000000..97cd79f --- /dev/null +++ b/CleanArchitecture.Shared/Tenants/TenantViewModel.cs @@ -0,0 +1,7 @@ +using System; + +namespace CleanArchitecture.Shared.Tenants; + +public sealed record TenantViewModel( + Guid Id, + string Name); \ No newline at end of file diff --git a/CleanArchitecture.gRPC.Tests/Users/GetUsersByIdsTests.cs b/CleanArchitecture.gRPC.Tests/Users/GetUsersByIdsTests.cs index 7022833..1c0e333 100644 --- a/CleanArchitecture.gRPC.Tests/Users/GetUsersByIdsTests.cs +++ b/CleanArchitecture.gRPC.Tests/Users/GetUsersByIdsTests.cs @@ -62,9 +62,9 @@ public sealed class GetUsersByIdsTests : IClassFixture } } - private static GetByIdsRequest SetupRequest(IEnumerable ids) + private static GetUsersByIdsRequest SetupRequest(IEnumerable ids) { - var request = new GetByIdsRequest(); + var request = new GetUsersByIdsRequest(); request.Ids.AddRange(ids.Select(id => id.ToString())); request.Ids.Add("Not a guid"); diff --git a/CleanArchitecture.gRPC/CleanArchitecture.cs b/CleanArchitecture.gRPC/CleanArchitecture.cs index 649b2fc..8aa7853 100644 --- a/CleanArchitecture.gRPC/CleanArchitecture.cs +++ b/CleanArchitecture.gRPC/CleanArchitecture.cs @@ -5,11 +5,17 @@ namespace CleanArchitecture.gRPC; public sealed class CleanArchitecture : ICleanArchitecture { private readonly IUsersContext _users; + private readonly ITenantsContext _tenants; public IUsersContext Users => _users; + public ITenantsContext Tenants => _tenants; - public CleanArchitecture(IUsersContext users) + public CleanArchitecture( + IUsersContext users, + ITenantsContext tenants) { _users = users; + _tenants = tenants; + } } diff --git a/CleanArchitecture.gRPC/Contexts/TenantsContext.cs b/CleanArchitecture.gRPC/Contexts/TenantsContext.cs new file mode 100644 index 0000000..a8235ad --- /dev/null +++ b/CleanArchitecture.gRPC/Contexts/TenantsContext.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using CleanArchitecture.gRPC.Interfaces; +using CleanArchitecture.Proto.Tenants; +using CleanArchitecture.Shared.Tenants; + +namespace CleanArchitecture.gRPC.Contexts; + +public sealed class TenantsContext : ITenantsContext +{ + private readonly TenantsApi.TenantsApiClient _client; + + public TenantsContext(TenantsApi.TenantsApiClient client) + { + _client = client; + } + + public async Task> GetTenantsByIds(IEnumerable ids) + { + var request = new GetTenantsByIdsRequest(); + + request.Ids.AddRange(ids.Select(id => id.ToString())); + + var result = await _client.GetByIdsAsync(request); + + return result.Tenants.Select(tenant => new TenantViewModel( + Guid.Parse(tenant.Id), + tenant.Name)); + } +} \ No newline at end of file diff --git a/CleanArchitecture.gRPC/Contexts/UsersContext.cs b/CleanArchitecture.gRPC/Contexts/UsersContext.cs index 4822231..a00cbb7 100644 --- a/CleanArchitecture.gRPC/Contexts/UsersContext.cs +++ b/CleanArchitecture.gRPC/Contexts/UsersContext.cs @@ -19,7 +19,7 @@ public sealed class UsersContext : IUsersContext public async Task> GetUsersByIds(IEnumerable ids) { - var request = new GetByIdsRequest(); + var request = new GetUsersByIdsRequest(); request.Ids.AddRange(ids.Select(id => id.ToString())); diff --git a/CleanArchitecture.gRPC/Extensions/ServiceCollectionExtensions.cs b/CleanArchitecture.gRPC/Extensions/ServiceCollectionExtensions.cs index 72d5c3c..89a9084 100644 --- a/CleanArchitecture.gRPC/Extensions/ServiceCollectionExtensions.cs +++ b/CleanArchitecture.gRPC/Extensions/ServiceCollectionExtensions.cs @@ -1,6 +1,7 @@ using CleanArchitecture.gRPC.Contexts; using CleanArchitecture.gRPC.Interfaces; using CleanArchitecture.gRPC.Models; +using CleanArchitecture.Proto.Tenants; using CleanArchitecture.Proto.Users; using Grpc.Net.Client; using Microsoft.Extensions.Configuration; @@ -35,20 +36,24 @@ public static class ServiceCollectionExtensions public static IServiceCollection AddCleanArchitectureGrpcClient( this IServiceCollection services, - string tetraQueryApiUrl) + string gRPCUrl) { - if (string.IsNullOrWhiteSpace(tetraQueryApiUrl)) + if (string.IsNullOrWhiteSpace(gRPCUrl)) { return services; } - var channel = GrpcChannel.ForAddress(tetraQueryApiUrl); + var channel = GrpcChannel.ForAddress(gRPCUrl); var usersClient = new UsersApi.UsersApiClient(channel); services.AddSingleton(usersClient); - services.AddSingleton(); + var tenantsClient = new TenantsApi.TenantsApiClient(channel); + services.AddSingleton(tenantsClient); + services.AddSingleton(); + services.AddSingleton(); + return services; } } diff --git a/CleanArchitecture.gRPC/ICleanArchitecture.cs b/CleanArchitecture.gRPC/ICleanArchitecture.cs index 168df0d..9e04af9 100644 --- a/CleanArchitecture.gRPC/ICleanArchitecture.cs +++ b/CleanArchitecture.gRPC/ICleanArchitecture.cs @@ -5,4 +5,5 @@ namespace CleanArchitecture.gRPC; public interface ICleanArchitecture { IUsersContext Users { get; } + ITenantsContext Tenants { get; } } diff --git a/CleanArchitecture.gRPC/Interfaces/ITenantsContext.cs b/CleanArchitecture.gRPC/Interfaces/ITenantsContext.cs new file mode 100644 index 0000000..db07813 --- /dev/null +++ b/CleanArchitecture.gRPC/Interfaces/ITenantsContext.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using CleanArchitecture.Shared.Tenants; + +namespace CleanArchitecture.gRPC.Interfaces; + +public interface ITenantsContext +{ + Task> GetTenantsByIds(IEnumerable ids); +} \ No newline at end of file