0
0
mirror of https://github.com/alex289/CleanArchitecture.git synced 2025-08-22 19:28:34 +00:00

feat: Add endpoints for tenants

This commit is contained in:
alex289 2023-08-28 19:41:49 +02:00
parent 64fb1067e0
commit 816d92fc85
No known key found for this signature in database
GPG Key ID: 573F77CD2D87F863
84 changed files with 998 additions and 190 deletions

View File

@ -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<DomainNotification> notifications,
ITenantService tenantService) : base(notifications)
{
_tenantService = tenantService;
}
[HttpGet]
[SwaggerOperation("Get a list of all tenants")]
[SwaggerResponse(200, "Request successful", typeof(ResponseMessage<IEnumerable<TenantViewModel>>))]
public async Task<IActionResult> GetAllTenantsAsync()
{
var tenants = await _tenantService.GetAllTenantsAsync();
return Response(tenants);
}
[HttpGet("{id:guid}")]
[SwaggerOperation("Get a tenant by id")]
[SwaggerResponse(200, "Request successful", typeof(ResponseMessage<TenantViewModel>))]
public async Task<IActionResult> 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<Guid>))]
public async Task<IActionResult> 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<UpdateTenantViewModel>))]
public async Task<IActionResult> 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<Guid>))]
public async Task<IActionResult> DeleteTenantAsync([FromRoute] Guid id)
{
await _tenantService.DeleteTenantAsync(id);
return Response(id);
}
}

View File

@ -13,6 +13,7 @@ using Swashbuckle.AspNetCore.Annotations;
namespace CleanArchitecture.Api.Controllers; namespace CleanArchitecture.Api.Controllers;
[ApiController] [ApiController]
[Authorize]
[Route("/api/v1/[controller]")] [Route("/api/v1/[controller]")]
public sealed class UserController : ApiController public sealed class UserController : ApiController
{ {
@ -25,7 +26,6 @@ public sealed class UserController : ApiController
_userService = userService; _userService = userService;
} }
[Authorize]
[HttpGet] [HttpGet]
[SwaggerOperation("Get a list of all users")] [SwaggerOperation("Get a list of all users")]
[SwaggerResponse(200, "Request successful", typeof(ResponseMessage<IEnumerable<UserViewModel>>))] [SwaggerResponse(200, "Request successful", typeof(ResponseMessage<IEnumerable<UserViewModel>>))]
@ -35,7 +35,6 @@ public sealed class UserController : ApiController
return Response(users); return Response(users);
} }
[Authorize]
[HttpGet("{id:guid}")] [HttpGet("{id:guid}")]
[SwaggerOperation("Get a user by id")] [SwaggerOperation("Get a user by id")]
[SwaggerResponse(200, "Request successful", typeof(ResponseMessage<UserViewModel>))] [SwaggerResponse(200, "Request successful", typeof(ResponseMessage<UserViewModel>))]
@ -47,7 +46,6 @@ public sealed class UserController : ApiController
return Response(user); return Response(user);
} }
[Authorize]
[HttpGet("me")] [HttpGet("me")]
[SwaggerOperation("Get the current active user")] [SwaggerOperation("Get the current active user")]
[SwaggerResponse(200, "Request successful", typeof(ResponseMessage<UserViewModel>))] [SwaggerResponse(200, "Request successful", typeof(ResponseMessage<UserViewModel>))]
@ -58,6 +56,7 @@ public sealed class UserController : ApiController
} }
[HttpPost] [HttpPost]
[AllowAnonymous]
[SwaggerOperation("Create a new user")] [SwaggerOperation("Create a new user")]
[SwaggerResponse(200, "Request successful", typeof(ResponseMessage<Guid>))] [SwaggerResponse(200, "Request successful", typeof(ResponseMessage<Guid>))]
public async Task<IActionResult> CreateUserAsync([FromBody] CreateUserViewModel viewModel) public async Task<IActionResult> CreateUserAsync([FromBody] CreateUserViewModel viewModel)
@ -66,7 +65,6 @@ public sealed class UserController : ApiController
return Response(userId); return Response(userId);
} }
[Authorize]
[HttpDelete("{id:guid}")] [HttpDelete("{id:guid}")]
[SwaggerOperation("Delete a user")] [SwaggerOperation("Delete a user")]
[SwaggerResponse(200, "Request successful", typeof(ResponseMessage<Guid>))] [SwaggerResponse(200, "Request successful", typeof(ResponseMessage<Guid>))]
@ -76,7 +74,6 @@ public sealed class UserController : ApiController
return Response(id); return Response(id);
} }
[Authorize]
[HttpPut] [HttpPut]
[SwaggerOperation("Update a user")] [SwaggerOperation("Update a user")]
[SwaggerResponse(200, "Request successful", typeof(ResponseMessage<UpdateUserViewModel>))] [SwaggerResponse(200, "Request successful", typeof(ResponseMessage<UpdateUserViewModel>))]
@ -86,7 +83,6 @@ public sealed class UserController : ApiController
return Response(viewModel); return Response(viewModel);
} }
[Authorize]
[HttpPost("changePassword")] [HttpPost("changePassword")]
[SwaggerOperation("Change a password for the current active user")] [SwaggerOperation("Change a password for the current active user")]
[SwaggerResponse(200, "Request successful", typeof(ResponseMessage<ChangePasswordViewModel>))] [SwaggerResponse(200, "Request successful", typeof(ResponseMessage<ChangePasswordViewModel>))]
@ -97,6 +93,7 @@ public sealed class UserController : ApiController
} }
[HttpPost("login")] [HttpPost("login")]
[AllowAnonymous]
[SwaggerOperation("Get a signed token for a user")] [SwaggerOperation("Get a signed token for a user")]
[SwaggerResponse(200, "Request successful", typeof(ResponseMessage<string>))] [SwaggerResponse(200, "Request successful", typeof(ResponseMessage<string>))]
public async Task<IActionResult> LoginUserAsync([FromBody] LoginUserViewModel viewModel) public async Task<IActionResult> LoginUserAsync([FromBody] LoginUserViewModel viewModel)

View File

@ -14,4 +14,6 @@
<ProjectReference Include="..\CleanArchitecture.Proto\CleanArchitecture.Proto.csproj" /> <ProjectReference Include="..\CleanArchitecture.Proto\CleanArchitecture.Proto.csproj" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -1,8 +1,11 @@
using System.Collections.Generic; using System.Collections.Generic;
using CleanArchitecture.Application.Interfaces; 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.GetAll;
using CleanArchitecture.Application.Queries.Users.GetUserById; using CleanArchitecture.Application.Queries.Users.GetUserById;
using CleanArchitecture.Application.Services; using CleanArchitecture.Application.Services;
using CleanArchitecture.Application.ViewModels.Tenants;
using CleanArchitecture.Application.ViewModels.Users; using CleanArchitecture.Application.ViewModels.Users;
using MediatR; using MediatR;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
@ -14,15 +17,21 @@ public static class ServiceCollectionExtension
public static IServiceCollection AddServices(this IServiceCollection services) public static IServiceCollection AddServices(this IServiceCollection services)
{ {
services.AddScoped<IUserService, UserService>(); services.AddScoped<IUserService, UserService>();
services.AddScoped<ITenantService, TenantService>();
return services; return services;
} }
public static IServiceCollection AddQueryHandlers(this IServiceCollection services) public static IServiceCollection AddQueryHandlers(this IServiceCollection services)
{ {
// User
services.AddScoped<IRequestHandler<GetUserByIdQuery, UserViewModel?>, GetUserByIdQueryHandler>(); services.AddScoped<IRequestHandler<GetUserByIdQuery, UserViewModel?>, GetUserByIdQueryHandler>();
services.AddScoped<IRequestHandler<GetAllUsersQuery, IEnumerable<UserViewModel>>, GetAllUsersQueryHandler>(); services.AddScoped<IRequestHandler<GetAllUsersQuery, IEnumerable<UserViewModel>>, GetAllUsersQueryHandler>();
// Tenant
services.AddScoped<IRequestHandler<GetTenantByIdQuery, TenantViewModel?>, GetTenantByIdQueryHandler>();
services.AddScoped<IRequestHandler<GetAllTenantsQuery, IEnumerable<TenantViewModel>>, GetAllTenantsQueryHandler>();
return services; return services;
} }
} }

View File

@ -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<Guid> CreateTenantAsync(CreateTenantViewModel tenant);
public Task UpdateTenantAsync(UpdateTenantViewModel tenant);
public Task DeleteTenantAsync(Guid tenantId);
public Task<TenantViewModel?> GetTenantByIdAsync(Guid tenantId, bool deleted);
public Task<IEnumerable<TenantViewModel>> GetAllTenantsAsync();
}

View File

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

View File

@ -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<GetAllTenantsQuery, IEnumerable<TenantViewModel>>
{
private readonly ITenantRepository _tenantRepository;
public GetAllTenantsQueryHandler(ITenantRepository tenantRepository)
{
_tenantRepository = tenantRepository;
}
public async Task<IEnumerable<TenantViewModel>> Handle(
GetAllTenantsQuery request,
CancellationToken cancellationToken)
{
return await _tenantRepository
.GetAllNoTracking()
.Where(x => !x.Deleted)
.Select(x => TenantViewModel.FromTenant(x))
.ToListAsync(cancellationToken);
}
}

View File

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

View File

@ -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<GetTenantByIdQuery, TenantViewModel?>
{
private readonly ITenantRepository _tenantRepository;
private readonly IMediatorHandler _bus;
public GetTenantByIdQueryHandler(ITenantRepository tenantRepository, IMediatorHandler bus)
{
_tenantRepository = tenantRepository;
_bus = bus;
}
public async Task<TenantViewModel?> 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);
}
}

View File

@ -30,7 +30,7 @@ public sealed class GetUserByIdQueryHandler :
x.Id == request.UserId && x.Id == request.UserId &&
x.Deleted == request.IsDeleted); x.Deleted == request.IsDeleted);
if (user == null) if (user is null)
{ {
await _bus.RaiseEventAsync( await _bus.RaiseEventAsync(
new DomainNotification( new DomainNotification(

View File

@ -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<Guid> 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<TenantViewModel?> GetTenantByIdAsync(Guid tenantId, bool deleted)
{
return await _bus.QueryAsync(new GetTenantByIdQuery(tenantId, deleted));
}
public async Task<IEnumerable<TenantViewModel>> GetAllTenantsAsync()
{
return await _bus.QueryAsync(new GetAllTenantsQuery());
}
}

View File

@ -0,0 +1,3 @@
namespace CleanArchitecture.Application.ViewModels.Tenants;
public sealed record CreateTenantViewModel(string Name);

View File

@ -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<UserViewModel> Users { get; set; } = new List<UserViewModel>();
public static TenantViewModel FromTenant(Tenant tenant)
{
return new TenantViewModel
{
Id = tenant.Id,
Name = tenant.Name,
Users = tenant.Users.Select(UserViewModel.FromUser)
};
}
}

View File

@ -0,0 +1,7 @@
using System;
namespace CleanArchitecture.Application.ViewModels.Tenants;
public sealed record UpdateTenantViewModel(
Guid Id,
string Name);

View File

@ -18,8 +18,8 @@ public sealed class UsersApiImplementation : UsersApi.UsersApiBase
_userRepository = userRepository; _userRepository = userRepository;
} }
public override async Task<GetByIdsResult> GetByIds( public override async Task<GetUsersByIdsResult> GetByIds(
GetByIdsRequest request, GetUsersByIdsRequest request,
ServerCallContext context) ServerCallContext context)
{ {
var idsAsGuids = new List<Guid>(request.Ids.Count); var idsAsGuids = new List<Guid>(request.Ids.Count);
@ -45,7 +45,7 @@ public sealed class UsersApiImplementation : UsersApi.UsersApiBase
}) })
.ToListAsync(); .ToListAsync();
var result = new GetByIdsResult(); var result = new GetUsersByIdsResult();
result.Users.AddRange(users); result.Users.AddRange(users);

View File

@ -22,7 +22,7 @@ public sealed class ChangePasswordCommandHandlerTests
_fixture _fixture
.VerifyNoDomainNotification() .VerifyNoDomainNotification()
.VerifyCommit() .VerifyCommit()
.VerifyRaisedEvent<PasswordChangedEvent>(x => x.UserId == user.Id); .VerifyRaisedEvent<PasswordChangedEvent>(x => x.AggregateId == user.Id);
} }
[Fact] [Fact]
@ -40,7 +40,7 @@ public sealed class ChangePasswordCommandHandlerTests
.VerifyAnyDomainNotification() .VerifyAnyDomainNotification()
.VerifyExistingNotification( .VerifyExistingNotification(
ErrorCodes.ObjectNotFound, ErrorCodes.ObjectNotFound,
$"There is no User with Id {userId}"); $"There is no user with Id {userId}");
} }
[Fact] [Fact]
@ -57,7 +57,7 @@ public sealed class ChangePasswordCommandHandlerTests
.VerifyNoRaisedEvent<UserUpdatedEvent>() .VerifyNoRaisedEvent<UserUpdatedEvent>()
.VerifyAnyDomainNotification() .VerifyAnyDomainNotification()
.VerifyExistingNotification( .VerifyExistingNotification(
DomainErrorCodes.UserPasswordIncorrect, DomainErrorCodes.User.UserPasswordIncorrect,
"The password is incorrect"); "The password is incorrect");
} }
} }

View File

@ -28,12 +28,12 @@ public sealed class ChangePasswordCommandValidationTests :
var errors = new List<string> var errors = new List<string>
{ {
DomainErrorCodes.UserEmptyPassword, DomainErrorCodes.User.UserEmptyPassword,
DomainErrorCodes.UserSpecialCharPassword, DomainErrorCodes.User.UserSpecialCharPassword,
DomainErrorCodes.UserNumberPassword, DomainErrorCodes.User.UserNumberPassword,
DomainErrorCodes.UserLowercaseLetterPassword, DomainErrorCodes.User.UserLowercaseLetterPassword,
DomainErrorCodes.UserUppercaseLetterPassword, DomainErrorCodes.User.UserUppercaseLetterPassword,
DomainErrorCodes.UserShortPassword DomainErrorCodes.User.UserShortPassword
}; };
ShouldHaveExpectedErrors(command, errors.ToArray()); ShouldHaveExpectedErrors(command, errors.ToArray());
@ -44,7 +44,7 @@ public sealed class ChangePasswordCommandValidationTests :
{ {
var command = CreateTestCommand("z8tnayvd5FNLU9AQm"); var command = CreateTestCommand("z8tnayvd5FNLU9AQm");
ShouldHaveSingleError(command, DomainErrorCodes.UserSpecialCharPassword); ShouldHaveSingleError(command, DomainErrorCodes.User.UserSpecialCharPassword);
} }
[Fact] [Fact]
@ -52,7 +52,7 @@ public sealed class ChangePasswordCommandValidationTests :
{ {
var command = CreateTestCommand("z]tnayvdFNLU:]AQm"); var command = CreateTestCommand("z]tnayvdFNLU:]AQm");
ShouldHaveSingleError(command, DomainErrorCodes.UserNumberPassword); ShouldHaveSingleError(command, DomainErrorCodes.User.UserNumberPassword);
} }
[Fact] [Fact]
@ -60,7 +60,7 @@ public sealed class ChangePasswordCommandValidationTests :
{ {
var command = CreateTestCommand("Z8]TNAYVDFNLU:]AQM"); var command = CreateTestCommand("Z8]TNAYVDFNLU:]AQM");
ShouldHaveSingleError(command, DomainErrorCodes.UserLowercaseLetterPassword); ShouldHaveSingleError(command, DomainErrorCodes.User.UserLowercaseLetterPassword);
} }
[Fact] [Fact]
@ -68,7 +68,7 @@ public sealed class ChangePasswordCommandValidationTests :
{ {
var command = CreateTestCommand("z8]tnayvd5fnlu9:]aqm"); var command = CreateTestCommand("z8]tnayvd5fnlu9:]aqm");
ShouldHaveSingleError(command, DomainErrorCodes.UserUppercaseLetterPassword); ShouldHaveSingleError(command, DomainErrorCodes.User.UserUppercaseLetterPassword);
} }
[Fact] [Fact]
@ -76,7 +76,7 @@ public sealed class ChangePasswordCommandValidationTests :
{ {
var command = CreateTestCommand("zA6{"); var command = CreateTestCommand("zA6{");
ShouldHaveSingleError(command, DomainErrorCodes.UserShortPassword); ShouldHaveSingleError(command, DomainErrorCodes.User.UserShortPassword);
} }
[Fact] [Fact]
@ -84,7 +84,7 @@ public sealed class ChangePasswordCommandValidationTests :
{ {
var command = CreateTestCommand(string.Concat(Enumerable.Repeat("zA6{", 12), 12)); var command = CreateTestCommand(string.Concat(Enumerable.Repeat("zA6{", 12), 12));
ShouldHaveSingleError(command, DomainErrorCodes.UserLongPassword); ShouldHaveSingleError(command, DomainErrorCodes.User.UserLongPassword);
} }
private static ChangePasswordCommand CreateTestCommand( private static ChangePasswordCommand CreateTestCommand(

View File

@ -28,7 +28,7 @@ public sealed class CreateUserCommandHandlerTests
_fixture _fixture
.VerifyNoDomainNotification() .VerifyNoDomainNotification()
.VerifyCommit() .VerifyCommit()
.VerifyRaisedEvent<UserCreatedEvent>(x => x.UserId == command.UserId); .VerifyRaisedEvent<UserCreatedEvent>(x => x.AggregateId == command.UserId);
} }
[Fact] [Fact]
@ -51,7 +51,7 @@ public sealed class CreateUserCommandHandlerTests
.VerifyNoRaisedEvent<UserCreatedEvent>() .VerifyNoRaisedEvent<UserCreatedEvent>()
.VerifyAnyDomainNotification() .VerifyAnyDomainNotification()
.VerifyExistingNotification( .VerifyExistingNotification(
DomainErrorCodes.UserAlreadyExists, DomainErrorCodes.User.UserAlreadyExists,
$"There is already a User with Id {command.UserId}"); $"There is already a user with Id {command.UserId}");
} }
} }

View File

@ -29,7 +29,7 @@ public sealed class CreateUserCommandValidationTests :
ShouldHaveSingleError( ShouldHaveSingleError(
command, command,
DomainErrorCodes.UserEmptyId, DomainErrorCodes.User.UserEmptyId,
"User id may not be empty"); "User id may not be empty");
} }
@ -40,7 +40,7 @@ public sealed class CreateUserCommandValidationTests :
ShouldHaveSingleError( ShouldHaveSingleError(
command, command,
DomainErrorCodes.UserInvalidEmail, DomainErrorCodes.User.UserInvalidEmail,
"Email is not a valid email address"); "Email is not a valid email address");
} }
@ -51,7 +51,7 @@ public sealed class CreateUserCommandValidationTests :
ShouldHaveSingleError( ShouldHaveSingleError(
command, command,
DomainErrorCodes.UserInvalidEmail, DomainErrorCodes.User.UserInvalidEmail,
"Email is not a valid email address"); "Email is not a valid email address");
} }
@ -62,7 +62,7 @@ public sealed class CreateUserCommandValidationTests :
ShouldHaveSingleError( ShouldHaveSingleError(
command, command,
DomainErrorCodes.UserEmailExceedsMaxLength, DomainErrorCodes.User.UserEmailExceedsMaxLength,
"Email may not be longer than 320 characters"); "Email may not be longer than 320 characters");
} }
@ -73,7 +73,7 @@ public sealed class CreateUserCommandValidationTests :
ShouldHaveSingleError( ShouldHaveSingleError(
command, command,
DomainErrorCodes.UserEmptyFirstName, DomainErrorCodes.User.UserEmptyFirstName,
"FirstName may not be empty"); "FirstName may not be empty");
} }
@ -84,7 +84,7 @@ public sealed class CreateUserCommandValidationTests :
ShouldHaveSingleError( ShouldHaveSingleError(
command, command,
DomainErrorCodes.UserFirstNameExceedsMaxLength, DomainErrorCodes.User.UserFirstNameExceedsMaxLength,
"FirstName may not be longer than 100 characters"); "FirstName may not be longer than 100 characters");
} }
@ -95,7 +95,7 @@ public sealed class CreateUserCommandValidationTests :
ShouldHaveSingleError( ShouldHaveSingleError(
command, command,
DomainErrorCodes.UserEmptyLastName, DomainErrorCodes.User.UserEmptyLastName,
"LastName may not be empty"); "LastName may not be empty");
} }
@ -106,7 +106,7 @@ public sealed class CreateUserCommandValidationTests :
ShouldHaveSingleError( ShouldHaveSingleError(
command, command,
DomainErrorCodes.UserLastNameExceedsMaxLength, DomainErrorCodes.User.UserLastNameExceedsMaxLength,
"LastName may not be longer than 100 characters"); "LastName may not be longer than 100 characters");
} }
@ -117,12 +117,12 @@ public sealed class CreateUserCommandValidationTests :
var errors = new List<string> var errors = new List<string>
{ {
DomainErrorCodes.UserEmptyPassword, DomainErrorCodes.User.UserEmptyPassword,
DomainErrorCodes.UserSpecialCharPassword, DomainErrorCodes.User.UserSpecialCharPassword,
DomainErrorCodes.UserNumberPassword, DomainErrorCodes.User.UserNumberPassword,
DomainErrorCodes.UserLowercaseLetterPassword, DomainErrorCodes.User.UserLowercaseLetterPassword,
DomainErrorCodes.UserUppercaseLetterPassword, DomainErrorCodes.User.UserUppercaseLetterPassword,
DomainErrorCodes.UserShortPassword DomainErrorCodes.User.UserShortPassword
}; };
ShouldHaveExpectedErrors(command, errors.ToArray()); ShouldHaveExpectedErrors(command, errors.ToArray());
@ -133,7 +133,7 @@ public sealed class CreateUserCommandValidationTests :
{ {
var command = CreateTestCommand(password: "z8tnayvd5FNLU9AQm"); var command = CreateTestCommand(password: "z8tnayvd5FNLU9AQm");
ShouldHaveSingleError(command, DomainErrorCodes.UserSpecialCharPassword); ShouldHaveSingleError(command, DomainErrorCodes.User.UserSpecialCharPassword);
} }
[Fact] [Fact]
@ -141,7 +141,7 @@ public sealed class CreateUserCommandValidationTests :
{ {
var command = CreateTestCommand(password: "z]tnayvdFNLU:]AQm"); var command = CreateTestCommand(password: "z]tnayvdFNLU:]AQm");
ShouldHaveSingleError(command, DomainErrorCodes.UserNumberPassword); ShouldHaveSingleError(command, DomainErrorCodes.User.UserNumberPassword);
} }
[Fact] [Fact]
@ -149,7 +149,7 @@ public sealed class CreateUserCommandValidationTests :
{ {
var command = CreateTestCommand(password: "Z8]TNAYVDFNLU:]AQM"); var command = CreateTestCommand(password: "Z8]TNAYVDFNLU:]AQM");
ShouldHaveSingleError(command, DomainErrorCodes.UserLowercaseLetterPassword); ShouldHaveSingleError(command, DomainErrorCodes.User.UserLowercaseLetterPassword);
} }
[Fact] [Fact]
@ -157,7 +157,7 @@ public sealed class CreateUserCommandValidationTests :
{ {
var command = CreateTestCommand(password: "z8]tnayvd5fnlu9:]aqm"); var command = CreateTestCommand(password: "z8]tnayvd5fnlu9:]aqm");
ShouldHaveSingleError(command, DomainErrorCodes.UserUppercaseLetterPassword); ShouldHaveSingleError(command, DomainErrorCodes.User.UserUppercaseLetterPassword);
} }
[Fact] [Fact]
@ -165,7 +165,7 @@ public sealed class CreateUserCommandValidationTests :
{ {
var command = CreateTestCommand(password: "zA6{"); var command = CreateTestCommand(password: "zA6{");
ShouldHaveSingleError(command, DomainErrorCodes.UserShortPassword); ShouldHaveSingleError(command, DomainErrorCodes.User.UserShortPassword);
} }
[Fact] [Fact]
@ -173,7 +173,7 @@ public sealed class CreateUserCommandValidationTests :
{ {
var command = CreateTestCommand(password: string.Concat(Enumerable.Repeat("zA6{", 12), 12)); var command = CreateTestCommand(password: string.Concat(Enumerable.Repeat("zA6{", 12), 12));
ShouldHaveSingleError(command, DomainErrorCodes.UserLongPassword); ShouldHaveSingleError(command, DomainErrorCodes.User.UserLongPassword);
} }
private static CreateUserCommand CreateTestCommand( private static CreateUserCommand CreateTestCommand(

View File

@ -22,7 +22,7 @@ public sealed class DeleteUserCommandHandlerTests
_fixture _fixture
.VerifyNoDomainNotification() .VerifyNoDomainNotification()
.VerifyCommit() .VerifyCommit()
.VerifyRaisedEvent<UserDeletedEvent>(x => x.UserId == user.Id); .VerifyRaisedEvent<UserDeletedEvent>(x => x.AggregateId == user.Id);
} }
[Fact] [Fact]
@ -40,6 +40,6 @@ public sealed class DeleteUserCommandHandlerTests
.VerifyAnyDomainNotification() .VerifyAnyDomainNotification()
.VerifyExistingNotification( .VerifyExistingNotification(
ErrorCodes.ObjectNotFound, ErrorCodes.ObjectNotFound,
$"There is no User with Id {command.UserId}"); $"There is no user with Id {command.UserId}");
} }
} }

View File

@ -27,7 +27,7 @@ public sealed class DeleteUserCommandValidationTests :
ShouldHaveSingleError( ShouldHaveSingleError(
command, command,
DomainErrorCodes.UserEmptyId, DomainErrorCodes.User.UserEmptyId,
"User id may not be empty"); "User id may not be empty");
} }

View File

@ -57,7 +57,7 @@ public sealed class LoginUserCommandHandlerTests
.VerifyAnyDomainNotification() .VerifyAnyDomainNotification()
.VerifyExistingNotification( .VerifyExistingNotification(
ErrorCodes.ObjectNotFound, ErrorCodes.ObjectNotFound,
$"There is no User with Email {command.Email}"); $"There is no user with email {command.Email}");
token.Should().BeEmpty(); token.Should().BeEmpty();
} }
@ -74,7 +74,7 @@ public sealed class LoginUserCommandHandlerTests
_fixture _fixture
.VerifyAnyDomainNotification() .VerifyAnyDomainNotification()
.VerifyExistingNotification( .VerifyExistingNotification(
DomainErrorCodes.UserPasswordIncorrect, DomainErrorCodes.User.UserPasswordIncorrect,
"The password is incorrect"); "The password is incorrect");
token.Should().BeEmpty(); token.Should().BeEmpty();

View File

@ -28,7 +28,7 @@ public sealed class LoginUserCommandValidationTests :
ShouldHaveSingleError( ShouldHaveSingleError(
command, command,
DomainErrorCodes.UserInvalidEmail, DomainErrorCodes.User.UserInvalidEmail,
"Email is not a valid email address"); "Email is not a valid email address");
} }
@ -39,7 +39,7 @@ public sealed class LoginUserCommandValidationTests :
ShouldHaveSingleError( ShouldHaveSingleError(
command, command,
DomainErrorCodes.UserInvalidEmail, DomainErrorCodes.User.UserInvalidEmail,
"Email is not a valid email address"); "Email is not a valid email address");
} }
@ -50,7 +50,7 @@ public sealed class LoginUserCommandValidationTests :
ShouldHaveSingleError( ShouldHaveSingleError(
command, command,
DomainErrorCodes.UserEmailExceedsMaxLength, DomainErrorCodes.User.UserEmailExceedsMaxLength,
"Email may not be longer than 320 characters"); "Email may not be longer than 320 characters");
} }
@ -61,12 +61,12 @@ public sealed class LoginUserCommandValidationTests :
var errors = new List<string> var errors = new List<string>
{ {
DomainErrorCodes.UserEmptyPassword, DomainErrorCodes.User.UserEmptyPassword,
DomainErrorCodes.UserSpecialCharPassword, DomainErrorCodes.User.UserSpecialCharPassword,
DomainErrorCodes.UserNumberPassword, DomainErrorCodes.User.UserNumberPassword,
DomainErrorCodes.UserLowercaseLetterPassword, DomainErrorCodes.User.UserLowercaseLetterPassword,
DomainErrorCodes.UserUppercaseLetterPassword, DomainErrorCodes.User.UserUppercaseLetterPassword,
DomainErrorCodes.UserShortPassword DomainErrorCodes.User.UserShortPassword
}; };
ShouldHaveExpectedErrors(command, errors.ToArray()); ShouldHaveExpectedErrors(command, errors.ToArray());
@ -77,7 +77,7 @@ public sealed class LoginUserCommandValidationTests :
{ {
var command = CreateTestCommand(password: "z8tnayvd5FNLU9AQm"); var command = CreateTestCommand(password: "z8tnayvd5FNLU9AQm");
ShouldHaveSingleError(command, DomainErrorCodes.UserSpecialCharPassword); ShouldHaveSingleError(command, DomainErrorCodes.User.UserSpecialCharPassword);
} }
[Fact] [Fact]
@ -85,7 +85,7 @@ public sealed class LoginUserCommandValidationTests :
{ {
var command = CreateTestCommand(password: "z]tnayvdFNLU:]AQm"); var command = CreateTestCommand(password: "z]tnayvdFNLU:]AQm");
ShouldHaveSingleError(command, DomainErrorCodes.UserNumberPassword); ShouldHaveSingleError(command, DomainErrorCodes.User.UserNumberPassword);
} }
[Fact] [Fact]
@ -93,7 +93,7 @@ public sealed class LoginUserCommandValidationTests :
{ {
var command = CreateTestCommand(password: "Z8]TNAYVDFNLU:]AQM"); var command = CreateTestCommand(password: "Z8]TNAYVDFNLU:]AQM");
ShouldHaveSingleError(command, DomainErrorCodes.UserLowercaseLetterPassword); ShouldHaveSingleError(command, DomainErrorCodes.User.UserLowercaseLetterPassword);
} }
[Fact] [Fact]
@ -101,7 +101,7 @@ public sealed class LoginUserCommandValidationTests :
{ {
var command = CreateTestCommand(password: "z8]tnayvd5fnlu9:]aqm"); var command = CreateTestCommand(password: "z8]tnayvd5fnlu9:]aqm");
ShouldHaveSingleError(command, DomainErrorCodes.UserUppercaseLetterPassword); ShouldHaveSingleError(command, DomainErrorCodes.User.UserUppercaseLetterPassword);
} }
[Fact] [Fact]
@ -109,7 +109,7 @@ public sealed class LoginUserCommandValidationTests :
{ {
var command = CreateTestCommand(password: "zA6{"); var command = CreateTestCommand(password: "zA6{");
ShouldHaveSingleError(command, DomainErrorCodes.UserShortPassword); ShouldHaveSingleError(command, DomainErrorCodes.User.UserShortPassword);
} }
[Fact] [Fact]
@ -117,7 +117,7 @@ public sealed class LoginUserCommandValidationTests :
{ {
var command = CreateTestCommand(password: string.Concat(Enumerable.Repeat("zA6{", 12), 12)); var command = CreateTestCommand(password: string.Concat(Enumerable.Repeat("zA6{", 12), 12));
ShouldHaveSingleError(command, DomainErrorCodes.UserLongPassword); ShouldHaveSingleError(command, DomainErrorCodes.User.UserLongPassword);
} }
private static LoginUserCommand CreateTestCommand( private static LoginUserCommand CreateTestCommand(

View File

@ -30,7 +30,7 @@ public sealed class UpdateUserCommandHandlerTests
_fixture _fixture
.VerifyNoDomainNotification() .VerifyNoDomainNotification()
.VerifyCommit() .VerifyCommit()
.VerifyRaisedEvent<UserUpdatedEvent>(x => x.UserId == command.UserId); .VerifyRaisedEvent<UserUpdatedEvent>(x => x.AggregateId == command.UserId);
} }
[Fact] [Fact]
@ -53,7 +53,7 @@ public sealed class UpdateUserCommandHandlerTests
.VerifyAnyDomainNotification() .VerifyAnyDomainNotification()
.VerifyExistingNotification( .VerifyExistingNotification(
ErrorCodes.ObjectNotFound, ErrorCodes.ObjectNotFound,
$"There is no User with Id {command.UserId}"); $"There is no user with Id {command.UserId}");
} }
[Fact] [Fact]
@ -86,7 +86,7 @@ public sealed class UpdateUserCommandHandlerTests
.VerifyNoRaisedEvent<UserUpdatedEvent>() .VerifyNoRaisedEvent<UserUpdatedEvent>()
.VerifyAnyDomainNotification() .VerifyAnyDomainNotification()
.VerifyExistingNotification( .VerifyExistingNotification(
DomainErrorCodes.UserAlreadyExists, DomainErrorCodes.User.UserAlreadyExists,
$"There is already a User with Email {command.Email}"); $"There is already a user with email {command.Email}");
} }
} }

View File

@ -28,7 +28,7 @@ public sealed class UpdateUserCommandValidationTests :
ShouldHaveSingleError( ShouldHaveSingleError(
command, command,
DomainErrorCodes.UserEmptyId, DomainErrorCodes.User.UserEmptyId,
"User id may not be empty"); "User id may not be empty");
} }
@ -39,7 +39,7 @@ public sealed class UpdateUserCommandValidationTests :
ShouldHaveSingleError( ShouldHaveSingleError(
command, command,
DomainErrorCodes.UserInvalidEmail, DomainErrorCodes.User.UserInvalidEmail,
"Email is not a valid email address"); "Email is not a valid email address");
} }
@ -50,7 +50,7 @@ public sealed class UpdateUserCommandValidationTests :
ShouldHaveSingleError( ShouldHaveSingleError(
command, command,
DomainErrorCodes.UserInvalidEmail, DomainErrorCodes.User.UserInvalidEmail,
"Email is not a valid email address"); "Email is not a valid email address");
} }
@ -61,7 +61,7 @@ public sealed class UpdateUserCommandValidationTests :
ShouldHaveSingleError( ShouldHaveSingleError(
command, command,
DomainErrorCodes.UserEmailExceedsMaxLength, DomainErrorCodes.User.UserEmailExceedsMaxLength,
"Email may not be longer than 320 characters"); "Email may not be longer than 320 characters");
} }
@ -72,7 +72,7 @@ public sealed class UpdateUserCommandValidationTests :
ShouldHaveSingleError( ShouldHaveSingleError(
command, command,
DomainErrorCodes.UserEmptyFirstName, DomainErrorCodes.User.UserEmptyFirstName,
"FirstName may not be empty"); "FirstName may not be empty");
} }
@ -83,7 +83,7 @@ public sealed class UpdateUserCommandValidationTests :
ShouldHaveSingleError( ShouldHaveSingleError(
command, command,
DomainErrorCodes.UserFirstNameExceedsMaxLength, DomainErrorCodes.User.UserFirstNameExceedsMaxLength,
"FirstName may not be longer than 100 characters"); "FirstName may not be longer than 100 characters");
} }
@ -94,7 +94,7 @@ public sealed class UpdateUserCommandValidationTests :
ShouldHaveSingleError( ShouldHaveSingleError(
command, command,
DomainErrorCodes.UserEmptyLastName, DomainErrorCodes.User.UserEmptyLastName,
"LastName may not be empty"); "LastName may not be empty");
} }
@ -105,7 +105,7 @@ public sealed class UpdateUserCommandValidationTests :
ShouldHaveSingleError( ShouldHaveSingleError(
command, command,
DomainErrorCodes.UserLastNameExceedsMaxLength, DomainErrorCodes.User.UserLastNameExceedsMaxLength,
"LastName may not be longer than 100 characters"); "LastName may not be longer than 100 characters");
} }

View File

@ -55,12 +55,12 @@ public sealed class ApiUser : IUser
{ {
get get
{ {
if (_name != null) if (_name is not null)
{ {
return _name; return _name;
} }
var identity = _httpContextAccessor.HttpContext?.User.Identity; var identity = _httpContextAccessor.HttpContext?.User.Identity;
if (identity == null) if (identity is null)
{ {
_name = string.Empty; _name = string.Empty;
return string.Empty; return string.Empty;

View File

@ -62,7 +62,7 @@ public abstract class CommandHandlerBase
return true; return true;
} }
if (command.ValidationResult == null) if (command.ValidationResult is null)
{ {
throw new InvalidOperationException("Command is invalid and should therefore have a validation result"); throw new InvalidOperationException("Command is invalid and should therefore have a validation result");
} }

View File

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

View File

@ -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<CreateTenantCommand>
{
private readonly ITenantRepository _tenantRepository;
public CreateTenantCommandHandler(
IMediatorHandler bus,
IUnitOfWork unitOfWork,
INotificationHandler<DomainNotification> 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));
}
}
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,20 @@
using CleanArchitecture.Domain.Errors;
using FluentValidation;
namespace CleanArchitecture.Domain.Commands.Tenants.DeleteTenant;
public sealed class DeleteTenantCommandValidation : AbstractValidator<DeleteTenantCommand>
{
public DeleteTenantCommandValidation()
{
AddRuleForId();
}
private void AddRuleForId()
{
RuleFor(cmd => cmd.AggregateId)
.NotEmpty()
.WithErrorCode(DomainErrorCodes.Tenant.TenantEmptyId)
.WithMessage("Tenant id may not be empty");
}
}

View File

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

View File

@ -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<UpdateTenantCommand>
{
private readonly ITenantRepository _tenantRepository;
public UpdateTenantCommandHandler(
IMediatorHandler bus,
IUnitOfWork unitOfWork,
INotificationHandler<DomainNotification> 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));
}
}
}

View File

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

View File

@ -4,7 +4,7 @@ namespace CleanArchitecture.Domain.Commands.Users.ChangePassword;
public sealed class ChangePasswordCommand : CommandBase 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()) public ChangePasswordCommand(string password, string newPassword) : base(Guid.NewGuid())
{ {
@ -17,7 +17,7 @@ public sealed class ChangePasswordCommand : CommandBase
public override bool IsValid() public override bool IsValid()
{ {
ValidationResult = _validation.Validate(this); ValidationResult = s_validation.Validate(this);
return ValidationResult.IsValid; return ValidationResult.IsValid;
} }
} }

View File

@ -36,12 +36,12 @@ public sealed class ChangePasswordCommandHandler : CommandHandlerBase,
var user = await _userRepository.GetByIdAsync(_user.GetUserId()); var user = await _userRepository.GetByIdAsync(_user.GetUserId());
if (user == null) if (user is null)
{ {
await NotifyAsync( await NotifyAsync(
new DomainNotification( new DomainNotification(
request.MessageType, request.MessageType,
$"There is no User with Id {_user.GetUserId()}", $"There is no user with Id {_user.GetUserId()}",
ErrorCodes.ObjectNotFound)); ErrorCodes.ObjectNotFound));
return; return;
@ -53,7 +53,7 @@ public sealed class ChangePasswordCommandHandler : CommandHandlerBase,
new DomainNotification( new DomainNotification(
request.MessageType, request.MessageType,
"The password is incorrect", "The password is incorrect",
DomainErrorCodes.UserPasswordIncorrect)); DomainErrorCodes.User.UserPasswordIncorrect));
return; return;
} }

View File

@ -4,7 +4,7 @@ namespace CleanArchitecture.Domain.Commands.Users.CreateUser;
public sealed class CreateUserCommand : CommandBase public sealed class CreateUserCommand : CommandBase
{ {
private readonly CreateUserCommandValidation _validation = new(); private static readonly CreateUserCommandValidation s_validation = new();
public CreateUserCommand( public CreateUserCommand(
Guid userId, Guid userId,
@ -31,7 +31,7 @@ public sealed class CreateUserCommand : CommandBase
public override bool IsValid() public override bool IsValid()
{ {
ValidationResult = _validation.Validate(this); ValidationResult = s_validation.Validate(this);
return ValidationResult.IsValid; return ValidationResult.IsValid;
} }
} }

View File

@ -35,25 +35,25 @@ public sealed class CreateUserCommandHandler : CommandHandlerBase,
var existingUser = await _userRepository.GetByIdAsync(request.UserId); var existingUser = await _userRepository.GetByIdAsync(request.UserId);
if (existingUser != null) if (existingUser is not null)
{ {
await Bus.RaiseEventAsync( await Bus.RaiseEventAsync(
new DomainNotification( new DomainNotification(
request.MessageType, request.MessageType,
$"There is already a User with Id {request.UserId}", $"There is already a user with Id {request.UserId}",
DomainErrorCodes.UserAlreadyExists)); DomainErrorCodes.User.UserAlreadyExists));
return; return;
} }
existingUser = await _userRepository.GetByEmailAsync(request.Email); existingUser = await _userRepository.GetByEmailAsync(request.Email);
if (existingUser != null) if (existingUser is not null)
{ {
await Bus.RaiseEventAsync( await Bus.RaiseEventAsync(
new DomainNotification( new DomainNotification(
request.MessageType, request.MessageType,
$"There is already a User with Email {request.Email}", $"There is already a user with email {request.Email}",
DomainErrorCodes.UserAlreadyExists)); DomainErrorCodes.User.UserAlreadyExists));
return; return;
} }

View File

@ -20,7 +20,7 @@ public sealed class CreateUserCommandValidation : AbstractValidator<CreateUserCo
{ {
RuleFor(cmd => cmd.UserId) RuleFor(cmd => cmd.UserId)
.NotEmpty() .NotEmpty()
.WithErrorCode(DomainErrorCodes.UserEmptyId) .WithErrorCode(DomainErrorCodes.User.UserEmptyId)
.WithMessage("User id may not be empty"); .WithMessage("User id may not be empty");
} }
@ -28,10 +28,10 @@ public sealed class CreateUserCommandValidation : AbstractValidator<CreateUserCo
{ {
RuleFor(cmd => cmd.Email) RuleFor(cmd => cmd.Email)
.EmailAddress() .EmailAddress()
.WithErrorCode(DomainErrorCodes.UserInvalidEmail) .WithErrorCode(DomainErrorCodes.User.UserInvalidEmail)
.WithMessage("Email is not a valid email address") .WithMessage("Email is not a valid email address")
.MaximumLength(MaxLengths.User.Email) .MaximumLength(MaxLengths.User.Email)
.WithErrorCode(DomainErrorCodes.UserEmailExceedsMaxLength) .WithErrorCode(DomainErrorCodes.User.UserEmailExceedsMaxLength)
.WithMessage("Email may not be longer than 320 characters"); .WithMessage("Email may not be longer than 320 characters");
} }
@ -39,10 +39,10 @@ public sealed class CreateUserCommandValidation : AbstractValidator<CreateUserCo
{ {
RuleFor(cmd => cmd.FirstName) RuleFor(cmd => cmd.FirstName)
.NotEmpty() .NotEmpty()
.WithErrorCode(DomainErrorCodes.UserEmptyFirstName) .WithErrorCode(DomainErrorCodes.User.UserEmptyFirstName)
.WithMessage("FirstName may not be empty") .WithMessage("FirstName may not be empty")
.MaximumLength(MaxLengths.User.FirstName) .MaximumLength(MaxLengths.User.FirstName)
.WithErrorCode(DomainErrorCodes.UserFirstNameExceedsMaxLength) .WithErrorCode(DomainErrorCodes.User.UserFirstNameExceedsMaxLength)
.WithMessage("FirstName may not be longer than 100 characters"); .WithMessage("FirstName may not be longer than 100 characters");
} }
@ -50,10 +50,10 @@ public sealed class CreateUserCommandValidation : AbstractValidator<CreateUserCo
{ {
RuleFor(cmd => cmd.LastName) RuleFor(cmd => cmd.LastName)
.NotEmpty() .NotEmpty()
.WithErrorCode(DomainErrorCodes.UserEmptyLastName) .WithErrorCode(DomainErrorCodes.User.UserEmptyLastName)
.WithMessage("LastName may not be empty") .WithMessage("LastName may not be empty")
.MaximumLength(MaxLengths.User.LastName) .MaximumLength(MaxLengths.User.LastName)
.WithErrorCode(DomainErrorCodes.UserLastNameExceedsMaxLength) .WithErrorCode(DomainErrorCodes.User.UserLastNameExceedsMaxLength)
.WithMessage("LastName may not be longer than 100 characters"); .WithMessage("LastName may not be longer than 100 characters");
} }

View File

@ -4,7 +4,7 @@ namespace CleanArchitecture.Domain.Commands.Users.DeleteUser;
public sealed class DeleteUserCommand : CommandBase public sealed class DeleteUserCommand : CommandBase
{ {
private readonly DeleteUserCommandValidation _validation = new(); private static readonly DeleteUserCommandValidation s_validation = new();
public DeleteUserCommand(Guid userId) : base(userId) public DeleteUserCommand(Guid userId) : base(userId)
{ {
@ -15,7 +15,7 @@ public sealed class DeleteUserCommand : CommandBase
public override bool IsValid() public override bool IsValid()
{ {
ValidationResult = _validation.Validate(this); ValidationResult = s_validation.Validate(this);
return ValidationResult.IsValid; return ValidationResult.IsValid;
} }
} }

View File

@ -36,12 +36,12 @@ public sealed class DeleteUserCommandHandler : CommandHandlerBase,
var user = await _userRepository.GetByIdAsync(request.UserId); var user = await _userRepository.GetByIdAsync(request.UserId);
if (user == null) if (user is null)
{ {
await NotifyAsync( await NotifyAsync(
new DomainNotification( new DomainNotification(
request.MessageType, request.MessageType,
$"There is no User with Id {request.UserId}", $"There is no user with Id {request.UserId}",
ErrorCodes.ObjectNotFound)); ErrorCodes.ObjectNotFound));
return; return;

View File

@ -14,7 +14,7 @@ public sealed class DeleteUserCommandValidation : AbstractValidator<DeleteUserCo
{ {
RuleFor(cmd => cmd.UserId) RuleFor(cmd => cmd.UserId)
.NotEmpty() .NotEmpty()
.WithErrorCode(DomainErrorCodes.UserEmptyId) .WithErrorCode(DomainErrorCodes.User.UserEmptyId)
.WithMessage("User id may not be empty"); .WithMessage("User id may not be empty");
} }
} }

View File

@ -6,7 +6,7 @@ namespace CleanArchitecture.Domain.Commands.Users.LoginUser;
public sealed class LoginUserCommand : CommandBase, public sealed class LoginUserCommand : CommandBase,
IRequest<string> IRequest<string>
{ {
private readonly LoginUserCommandValidation _validation = new(); private static readonly LoginUserCommandValidation s_validation = new();
public LoginUserCommand( public LoginUserCommand(
@ -22,7 +22,7 @@ public sealed class LoginUserCommand : CommandBase,
public override bool IsValid() public override bool IsValid()
{ {
ValidationResult = _validation.Validate(this); ValidationResult = s_validation.Validate(this);
return ValidationResult.IsValid; return ValidationResult.IsValid;
} }
} }

View File

@ -45,12 +45,12 @@ public sealed class LoginUserCommandHandler : CommandHandlerBase,
var user = await _userRepository.GetByEmailAsync(request.Email); var user = await _userRepository.GetByEmailAsync(request.Email);
if (user == null) if (user is null)
{ {
await NotifyAsync( await NotifyAsync(
new DomainNotification( new DomainNotification(
request.MessageType, request.MessageType,
$"There is no User with Email {request.Email}", $"There is no user with email {request.Email}",
ErrorCodes.ObjectNotFound)); ErrorCodes.ObjectNotFound));
return ""; return "";
@ -64,7 +64,7 @@ public sealed class LoginUserCommandHandler : CommandHandlerBase,
new DomainNotification( new DomainNotification(
request.MessageType, request.MessageType,
"The password is incorrect", "The password is incorrect",
DomainErrorCodes.UserPasswordIncorrect)); DomainErrorCodes.User.UserPasswordIncorrect));
return ""; return "";
} }

View File

@ -17,10 +17,10 @@ public sealed class LoginUserCommandValidation : AbstractValidator<LoginUserComm
{ {
RuleFor(cmd => cmd.Email) RuleFor(cmd => cmd.Email)
.EmailAddress() .EmailAddress()
.WithErrorCode(DomainErrorCodes.UserInvalidEmail) .WithErrorCode(DomainErrorCodes.User.UserInvalidEmail)
.WithMessage("Email is not a valid email address") .WithMessage("Email is not a valid email address")
.MaximumLength(MaxLengths.User.Email) .MaximumLength(MaxLengths.User.Email)
.WithErrorCode(DomainErrorCodes.UserEmailExceedsMaxLength) .WithErrorCode(DomainErrorCodes.User.UserEmailExceedsMaxLength)
.WithMessage("Email may not be longer than 320 characters"); .WithMessage("Email may not be longer than 320 characters");
} }

View File

@ -5,7 +5,7 @@ namespace CleanArchitecture.Domain.Commands.Users.UpdateUser;
public sealed class UpdateUserCommand : CommandBase public sealed class UpdateUserCommand : CommandBase
{ {
private readonly UpdateUserCommandValidation _validation = new(); private static readonly UpdateUserCommandValidation s_validation = new();
public UpdateUserCommand( public UpdateUserCommand(
Guid userId, Guid userId,
@ -29,7 +29,7 @@ public sealed class UpdateUserCommand : CommandBase
public override bool IsValid() public override bool IsValid()
{ {
ValidationResult = _validation.Validate(this); ValidationResult = s_validation.Validate(this);
return ValidationResult.IsValid; return ValidationResult.IsValid;
} }
} }

View File

@ -36,12 +36,12 @@ public sealed class UpdateUserCommandHandler : CommandHandlerBase,
var user = await _userRepository.GetByIdAsync(request.UserId); var user = await _userRepository.GetByIdAsync(request.UserId);
if (user == null) if (user is null)
{ {
await Bus.RaiseEventAsync( await Bus.RaiseEventAsync(
new DomainNotification( new DomainNotification(
request.MessageType, request.MessageType,
$"There is no User with Id {request.UserId}", $"There is no user with Id {request.UserId}",
ErrorCodes.ObjectNotFound)); ErrorCodes.ObjectNotFound));
return; return;
} }
@ -61,13 +61,13 @@ public sealed class UpdateUserCommandHandler : CommandHandlerBase,
{ {
var existingUser = await _userRepository.GetByEmailAsync(request.Email); var existingUser = await _userRepository.GetByEmailAsync(request.Email);
if (existingUser != null) if (existingUser is not null)
{ {
await Bus.RaiseEventAsync( await Bus.RaiseEventAsync(
new DomainNotification( new DomainNotification(
request.MessageType, request.MessageType,
$"There is already a User with Email {request.Email}", $"There is already a user with email {request.Email}",
DomainErrorCodes.UserAlreadyExists)); DomainErrorCodes.User.UserAlreadyExists));
return; return;
} }
} }

View File

@ -19,7 +19,7 @@ public sealed class UpdateUserCommandValidation : AbstractValidator<UpdateUserCo
{ {
RuleFor(cmd => cmd.UserId) RuleFor(cmd => cmd.UserId)
.NotEmpty() .NotEmpty()
.WithErrorCode(DomainErrorCodes.UserEmptyId) .WithErrorCode(DomainErrorCodes.User.UserEmptyId)
.WithMessage("User id may not be empty"); .WithMessage("User id may not be empty");
} }
@ -27,10 +27,10 @@ public sealed class UpdateUserCommandValidation : AbstractValidator<UpdateUserCo
{ {
RuleFor(cmd => cmd.Email) RuleFor(cmd => cmd.Email)
.EmailAddress() .EmailAddress()
.WithErrorCode(DomainErrorCodes.UserInvalidEmail) .WithErrorCode(DomainErrorCodes.User.UserInvalidEmail)
.WithMessage("Email is not a valid email address") .WithMessage("Email is not a valid email address")
.MaximumLength(MaxLengths.User.Email) .MaximumLength(MaxLengths.User.Email)
.WithErrorCode(DomainErrorCodes.UserEmailExceedsMaxLength) .WithErrorCode(DomainErrorCodes.User.UserEmailExceedsMaxLength)
.WithMessage("Email may not be longer than 320 characters"); .WithMessage("Email may not be longer than 320 characters");
} }
@ -38,10 +38,10 @@ public sealed class UpdateUserCommandValidation : AbstractValidator<UpdateUserCo
{ {
RuleFor(cmd => cmd.FirstName) RuleFor(cmd => cmd.FirstName)
.NotEmpty() .NotEmpty()
.WithErrorCode(DomainErrorCodes.UserEmptyFirstName) .WithErrorCode(DomainErrorCodes.User.UserEmptyFirstName)
.WithMessage("FirstName may not be empty") .WithMessage("FirstName may not be empty")
.MaximumLength(MaxLengths.User.FirstName) .MaximumLength(MaxLengths.User.FirstName)
.WithErrorCode(DomainErrorCodes.UserFirstNameExceedsMaxLength) .WithErrorCode(DomainErrorCodes.User.UserFirstNameExceedsMaxLength)
.WithMessage("FirstName may not be longer than 100 characters"); .WithMessage("FirstName may not be longer than 100 characters");
} }
@ -49,10 +49,10 @@ public sealed class UpdateUserCommandValidation : AbstractValidator<UpdateUserCo
{ {
RuleFor(cmd => cmd.LastName) RuleFor(cmd => cmd.LastName)
.NotEmpty() .NotEmpty()
.WithErrorCode(DomainErrorCodes.UserEmptyLastName) .WithErrorCode(DomainErrorCodes.User.UserEmptyLastName)
.WithMessage("LastName may not be empty") .WithMessage("LastName may not be empty")
.MaximumLength(MaxLengths.User.LastName) .MaximumLength(MaxLengths.User.LastName)
.WithErrorCode(DomainErrorCodes.UserLastNameExceedsMaxLength) .WithErrorCode(DomainErrorCodes.User.UserLastNameExceedsMaxLength)
.WithMessage("LastName may not be longer than 100 characters"); .WithMessage("LastName may not be longer than 100 characters");
} }
@ -60,7 +60,7 @@ public sealed class UpdateUserCommandValidation : AbstractValidator<UpdateUserCo
{ {
RuleFor(cmd => cmd.Role) RuleFor(cmd => cmd.Role)
.IsInEnum() .IsInEnum()
.WithErrorCode(DomainErrorCodes.UserInvalidRole) .WithErrorCode(DomainErrorCodes.User.UserInvalidRole)
.WithMessage("Role is not a valid role"); .WithMessage("Role is not a valid role");
} }
} }

View File

@ -15,4 +15,9 @@ public class Tenant : Entity
{ {
Name = name; Name = name;
} }
public void SetName(string name)
{
Name = name;
}
} }

View File

@ -2,26 +2,40 @@ namespace CleanArchitecture.Domain.Errors;
public static class DomainErrorCodes public static class DomainErrorCodes
{ {
// User Validation public static class User
public const string UserEmptyId = "USER_EMPTY_ID"; {
public const string UserEmptyFirstName = "USER_EMPTY_FIRST_NAME"; // User Validation
public const string UserEmptyLastName = "USER_EMPTY_LAST_NAME"; public const string UserEmptyId = "USER_EMPTY_ID";
public const string UserEmailExceedsMaxLength = "USER_EMAIL_EXCEEDS_MAX_LENGTH"; public const string UserEmptyFirstName = "USER_EMPTY_FIRST_NAME";
public const string UserFirstNameExceedsMaxLength = "USER_FIRST_NAME_EXCEEDS_MAX_LENGTH"; public const string UserEmptyLastName = "USER_EMPTY_LAST_NAME";
public const string UserLastNameExceedsMaxLength = "USER_LAST_NAME_EXCEEDS_MAX_LENGTH"; public const string UserEmailExceedsMaxLength = "USER_EMAIL_EXCEEDS_MAX_LENGTH";
public const string UserInvalidEmail = "USER_INVALID_EMAIL"; public const string UserFirstNameExceedsMaxLength = "USER_FIRST_NAME_EXCEEDS_MAX_LENGTH";
public const string UserInvalidRole = "USER_INVALID_ROLE"; 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 // User Password Validation
public const string UserEmptyPassword = "USER_PASSWORD_MAY_NOT_BE_EMPTY"; 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 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 UserLongPassword = "USER_PASSWORD_MAY_NOT_BE_LONGER_THAN_50_CHARACTERS";
public const string UserUppercaseLetterPassword = "USER_PASSWORD_MUST_CONTAIN_A_UPPERCASE_LETTER"; 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 UserLowercaseLetterPassword = "USER_PASSWORD_MUST_CONTAIN_A_LOWERCASE_LETTER";
public const string UserNumberPassword = "USER_PASSWORD_MUST_CONTAIN_A_NUMBER"; public const string UserNumberPassword = "USER_PASSWORD_MUST_CONTAIN_A_NUMBER";
public const string UserSpecialCharPassword = "USER_PASSWORD_MUST_CONTAIN_A_SPECIAL_CHARACTER"; public const string UserSpecialCharPassword = "USER_PASSWORD_MUST_CONTAIN_A_SPECIAL_CHARACTER";
// User // General
public const string UserAlreadyExists = "USER_ALREADY_EXISTS"; public const string UserAlreadyExists = "USER_ALREADY_EXISTS";
public const string UserPasswordIncorrect = "USER_PASSWORD_INCORRECT"; 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";
}
} }

View File

@ -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<TenantCreatedEvent>,
INotificationHandler<TenantDeletedEvent>,
INotificationHandler<TenantUpdatedEvent>
{
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;
}
}

View File

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

View File

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

View File

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

View File

@ -7,8 +7,5 @@ public sealed class PasswordChangedEvent : DomainEvent
{ {
public PasswordChangedEvent(Guid userId) : base(userId) public PasswordChangedEvent(Guid userId) : base(userId)
{ {
UserId = userId;
} }
public Guid UserId { get; }
} }

View File

@ -7,8 +7,5 @@ public sealed class UserCreatedEvent : DomainEvent
{ {
public UserCreatedEvent(Guid userId) : base(userId) public UserCreatedEvent(Guid userId) : base(userId)
{ {
UserId = userId;
} }
public Guid UserId { get; }
} }

View File

@ -7,8 +7,5 @@ public sealed class UserDeletedEvent : DomainEvent
{ {
public UserDeletedEvent(Guid userId) : base(userId) public UserDeletedEvent(Guid userId) : base(userId)
{ {
UserId = userId;
} }
public Guid UserId { get; }
} }

View File

@ -7,8 +7,5 @@ public sealed class UserUpdatedEvent : DomainEvent
{ {
public UserUpdatedEvent(Guid userId) : base(userId) public UserUpdatedEvent(Guid userId) : base(userId)
{ {
UserId = userId;
} }
public Guid UserId { get; }
} }

View File

@ -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.ChangePassword;
using CleanArchitecture.Domain.Commands.Users.CreateUser; using CleanArchitecture.Domain.Commands.Users.CreateUser;
using CleanArchitecture.Domain.Commands.Users.DeleteUser; using CleanArchitecture.Domain.Commands.Users.DeleteUser;
using CleanArchitecture.Domain.Commands.Users.LoginUser; using CleanArchitecture.Domain.Commands.Users.LoginUser;
using CleanArchitecture.Domain.Commands.Users.UpdateUser; using CleanArchitecture.Domain.Commands.Users.UpdateUser;
using CleanArchitecture.Domain.EventHandler; using CleanArchitecture.Domain.EventHandler;
using CleanArchitecture.Domain.Events.Tenant;
using CleanArchitecture.Domain.Events.User; using CleanArchitecture.Domain.Events.User;
using CleanArchitecture.Domain.Interfaces; using CleanArchitecture.Domain.Interfaces;
using MediatR; using MediatR;
@ -22,6 +26,10 @@ public static class ServiceCollectionExtension
services.AddScoped<IRequestHandler<ChangePasswordCommand>, ChangePasswordCommandHandler>(); services.AddScoped<IRequestHandler<ChangePasswordCommand>, ChangePasswordCommandHandler>();
services.AddScoped<IRequestHandler<LoginUserCommand, string>, LoginUserCommandHandler>(); services.AddScoped<IRequestHandler<LoginUserCommand, string>, LoginUserCommandHandler>();
// Tenant
services.AddScoped<IRequestHandler<CreateTenantCommand>, CreateTenantCommandHandler>();
services.AddScoped<IRequestHandler<UpdateTenantCommand>, UpdateTenantCommandHandler>();
services.AddScoped<IRequestHandler<DeleteTenantCommand>, DeleteTenantCommandHandler>();
return services; return services;
} }
@ -33,6 +41,11 @@ public static class ServiceCollectionExtension
services.AddScoped<INotificationHandler<UserUpdatedEvent>, UserEventHandler>(); services.AddScoped<INotificationHandler<UserUpdatedEvent>, UserEventHandler>();
services.AddScoped<INotificationHandler<UserDeletedEvent>, UserEventHandler>(); services.AddScoped<INotificationHandler<UserDeletedEvent>, UserEventHandler>();
services.AddScoped<INotificationHandler<PasswordChangedEvent>, UserEventHandler>(); services.AddScoped<INotificationHandler<PasswordChangedEvent>, UserEventHandler>();
// Tenant
services.AddScoped<INotificationHandler<TenantCreatedEvent>, TenantEventHandler>();
services.AddScoped<INotificationHandler<TenantUpdatedEvent>, TenantEventHandler>();
services.AddScoped<INotificationHandler<TenantDeletedEvent>, TenantEventHandler>();
return services; return services;
} }

View File

@ -23,13 +23,13 @@ public static partial class CustomValidator
int maxLength = 50) int maxLength = 50)
{ {
var options = ruleBuilder var options = ruleBuilder
.NotEmpty().WithErrorCode(DomainErrorCodes.UserEmptyPassword) .NotEmpty().WithErrorCode(DomainErrorCodes.User.UserEmptyPassword)
.MinimumLength(minLength).WithErrorCode(DomainErrorCodes.UserShortPassword) .MinimumLength(minLength).WithErrorCode(DomainErrorCodes.User.UserShortPassword)
.MaximumLength(maxLength).WithErrorCode(DomainErrorCodes.UserLongPassword) .MaximumLength(maxLength).WithErrorCode(DomainErrorCodes.User.UserLongPassword)
.Matches("[A-Z]").WithErrorCode(DomainErrorCodes.UserUppercaseLetterPassword) .Matches("[A-Z]").WithErrorCode(DomainErrorCodes.User.UserUppercaseLetterPassword)
.Matches("[a-z]").WithErrorCode(DomainErrorCodes.UserLowercaseLetterPassword) .Matches("[a-z]").WithErrorCode(DomainErrorCodes.User.UserLowercaseLetterPassword)
.Matches("[0-9]").WithErrorCode(DomainErrorCodes.UserNumberPassword) .Matches("[0-9]").WithErrorCode(DomainErrorCodes.User.UserNumberPassword)
.Matches("[^a-zA-Z0-9]").WithErrorCode(DomainErrorCodes.UserSpecialCharPassword); .Matches("[^a-zA-Z0-9]").WithErrorCode(DomainErrorCodes.User.UserSpecialCharPassword);
return options; return options;
} }

View File

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

View File

@ -0,0 +1,7 @@
using CleanArchitecture.Domain.Entities;
namespace CleanArchitecture.Domain.Interfaces.Repositories;
public interface ITenantRepository : IRepository<Tenant>
{
}

View File

@ -13,7 +13,7 @@ public sealed class EventStoreContext : IEventStoreContext
{ {
_user = user; _user = user;
if (httpContextAccessor?.HttpContext == null || if (httpContextAccessor?.HttpContext is null ||
!httpContextAccessor.HttpContext.Request.Headers.TryGetValue("X-CLEAN-ARCHITECTURE-CORRELATION-ID", out var id)) !httpContextAccessor.HttpContext.Request.Headers.TryGetValue("X-CLEAN-ARCHITECTURE-CORRELATION-ID", out var id))
{ {
_correlationId = $"internal - {Guid.NewGuid()}"; _correlationId = $"internal - {Guid.NewGuid()}";

View File

@ -46,6 +46,7 @@ public static class ServiceCollectionExtensions
// Repositories // Repositories
services.AddScoped<IUserRepository, UserRepository>(); services.AddScoped<IUserRepository, UserRepository>();
services.AddScoped<ITenantRepository, TenantRepository>();
return services; return services;
} }

View File

@ -55,9 +55,9 @@ public class BaseRepository<TEntity> : IRepository<TEntity> where TEntity : Enti
DbSet.Update(entity); DbSet.Update(entity);
} }
public Task<bool> ExistsAsync(Guid id) public async Task<bool> 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) public void Remove(TEntity entity, bool hardDelete = false)
@ -72,6 +72,20 @@ public class BaseRepository<TEntity> : IRepository<TEntity> where TEntity : Enti
DbSet.Update(entity); DbSet.Update(entity);
} }
} }
public void RemoveRange(IEnumerable<TEntity> entities, bool hardDelete = false)
{
if (hardDelete)
{
DbSet.RemoveRange(entities);
return;
}
foreach (var entity in entities)
{
entity.Delete();
}
}
public int SaveChanges() public int SaveChanges()
{ {

View File

@ -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<Tenant>, ITenantRepository
{
public TenantRepository(ApplicationDbContext context) : base(context)
{
}
}

View File

@ -15,7 +15,7 @@ public static class FunctionalTestsServiceCollectionExtensions
DbConnection connection) where TContext : DbContext DbConnection connection) where TContext : DbContext
{ {
var descriptor = services.SingleOrDefault(d => d.ServiceType == typeof(DbContextOptions<TContext>)); var descriptor = services.SingleOrDefault(d => d.ServiceType == typeof(DbContextOptions<TContext>));
if (descriptor != null) if (descriptor is not null)
services.Remove(descriptor); services.Remove(descriptor);
services.AddScoped(p => services.AddScoped(p =>

View File

@ -21,7 +21,7 @@ public sealed class GetUsersByIdsTests : IClassFixture<GetUsersByIdsTestFixture>
{ {
var client = new UsersApi.UsersApiClient(_fixture.GrpcChannel); var client = new UsersApi.UsersApiClient(_fixture.GrpcChannel);
var request = new GetByIdsRequest(); var request = new GetUsersByIdsRequest();
request.Ids.Add(_fixture.CreatedUserId.ToString()); request.Ids.Add(_fixture.CreatedUserId.ToString());
var response = await client.GetByIdsAsync(request); var response = await client.GetByIdsAsync(request);

View File

@ -5,14 +5,11 @@
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<None Remove="Users\Models.proto" />
<None Remove="Users\UsersApi.proto" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<Protobuf Include="Users\Models.proto" GrpcServices="Both" /> <Protobuf Include="Users\Models.proto" GrpcServices="Both" />
<Protobuf Include="Users\UsersApi.proto" GrpcServices="Both" /> <Protobuf Include="Users\UsersApi.proto" GrpcServices="Both" />
<Protobuf Include="Tenants\Models.proto" GrpcServices="Both" />
<Protobuf Include="Tenants\TenantsApi.proto" GrpcServices="Both" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

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

View File

@ -0,0 +1,9 @@
syntax = "proto3";
option csharp_namespace = "CleanArchitecture.Proto.Tenants";
import "Tenants/Models.proto";
service TenantsApi {
rpc GetByIds(GetTenantsByIdsRequest) returns (GetTenantsByIdsResult);
}

View File

@ -10,10 +10,10 @@ message GrpcUser {
bool isDeleted = 6; bool isDeleted = 6;
} }
message GetByIdsResult { message GetUsersByIdsResult {
repeated GrpcUser users = 1; repeated GrpcUser users = 1;
} }
message GetByIdsRequest { message GetUsersByIdsRequest {
repeated string ids = 1; repeated string ids = 1;
} }

View File

@ -5,5 +5,5 @@ option csharp_namespace = "CleanArchitecture.Proto.Users";
import "Users/Models.proto"; import "Users/Models.proto";
service UsersApi { service UsersApi {
rpc GetByIds(GetByIdsRequest) returns (GetByIdsResult); rpc GetByIds(GetUsersByIdsRequest) returns (GetUsersByIdsResult);
} }

View File

@ -5,4 +5,5 @@
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
</Project> </Project>

View File

@ -0,0 +1,7 @@
using System;
namespace CleanArchitecture.Shared.Tenants;
public sealed record TenantViewModel(
Guid Id,
string Name);

View File

@ -62,9 +62,9 @@ public sealed class GetUsersByIdsTests : IClassFixture<UserTestsFixture>
} }
} }
private static GetByIdsRequest SetupRequest(IEnumerable<Guid> ids) private static GetUsersByIdsRequest SetupRequest(IEnumerable<Guid> ids)
{ {
var request = new GetByIdsRequest(); var request = new GetUsersByIdsRequest();
request.Ids.AddRange(ids.Select(id => id.ToString())); request.Ids.AddRange(ids.Select(id => id.ToString()));
request.Ids.Add("Not a guid"); request.Ids.Add("Not a guid");

View File

@ -5,11 +5,17 @@ namespace CleanArchitecture.gRPC;
public sealed class CleanArchitecture : ICleanArchitecture public sealed class CleanArchitecture : ICleanArchitecture
{ {
private readonly IUsersContext _users; private readonly IUsersContext _users;
private readonly ITenantsContext _tenants;
public IUsersContext Users => _users; public IUsersContext Users => _users;
public ITenantsContext Tenants => _tenants;
public CleanArchitecture(IUsersContext users) public CleanArchitecture(
IUsersContext users,
ITenantsContext tenants)
{ {
_users = users; _users = users;
_tenants = tenants;
} }
} }

View File

@ -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<IEnumerable<TenantViewModel>> GetTenantsByIds(IEnumerable<Guid> 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));
}
}

View File

@ -19,7 +19,7 @@ public sealed class UsersContext : IUsersContext
public async Task<IEnumerable<UserViewModel>> GetUsersByIds(IEnumerable<Guid> ids) public async Task<IEnumerable<UserViewModel>> GetUsersByIds(IEnumerable<Guid> ids)
{ {
var request = new GetByIdsRequest(); var request = new GetUsersByIdsRequest();
request.Ids.AddRange(ids.Select(id => id.ToString())); request.Ids.AddRange(ids.Select(id => id.ToString()));

View File

@ -1,6 +1,7 @@
using CleanArchitecture.gRPC.Contexts; using CleanArchitecture.gRPC.Contexts;
using CleanArchitecture.gRPC.Interfaces; using CleanArchitecture.gRPC.Interfaces;
using CleanArchitecture.gRPC.Models; using CleanArchitecture.gRPC.Models;
using CleanArchitecture.Proto.Tenants;
using CleanArchitecture.Proto.Users; using CleanArchitecture.Proto.Users;
using Grpc.Net.Client; using Grpc.Net.Client;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
@ -35,20 +36,24 @@ public static class ServiceCollectionExtensions
public static IServiceCollection AddCleanArchitectureGrpcClient( public static IServiceCollection AddCleanArchitectureGrpcClient(
this IServiceCollection services, this IServiceCollection services,
string tetraQueryApiUrl) string gRPCUrl)
{ {
if (string.IsNullOrWhiteSpace(tetraQueryApiUrl)) if (string.IsNullOrWhiteSpace(gRPCUrl))
{ {
return services; return services;
} }
var channel = GrpcChannel.ForAddress(tetraQueryApiUrl); var channel = GrpcChannel.ForAddress(gRPCUrl);
var usersClient = new UsersApi.UsersApiClient(channel); var usersClient = new UsersApi.UsersApiClient(channel);
services.AddSingleton(usersClient); services.AddSingleton(usersClient);
services.AddSingleton<IUsersContext, UsersContext>(); var tenantsClient = new TenantsApi.TenantsApiClient(channel);
services.AddSingleton(tenantsClient);
services.AddSingleton<IUsersContext, UsersContext>();
services.AddSingleton<ITenantsContext, TenantsContext>();
return services; return services;
} }
} }

View File

@ -5,4 +5,5 @@ namespace CleanArchitecture.gRPC;
public interface ICleanArchitecture public interface ICleanArchitecture
{ {
IUsersContext Users { get; } IUsersContext Users { get; }
ITenantsContext Tenants { get; }
} }

View File

@ -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<IEnumerable<TenantViewModel>> GetTenantsByIds(IEnumerable<Guid> ids);
}