Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactoring CQRS structure for enhanced clarity and accessibility #205

Merged
merged 1 commit into from
Nov 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions application/account-management/Api/Tenants/TenantEndpoints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,15 @@ public static void MapTenantEndpoints(this IEndpointRouteBuilder routes)
var group = routes.MapGroup(RoutesPrefix);

group.MapGet("/{id}", async Task<ApiResult<TenantResponseDto>> (TenantId id, ISender mediator)
=> await mediator.Send(new GetTenant.Query(id)));
=> await mediator.Send(new GetTenantQuery(id)));

group.MapPost("/", async Task<ApiResult> (CreateTenant.Command command, ISender mediator)
group.MapPost("/", async Task<ApiResult> (CreateTenantCommand command, ISender mediator)
=> (await mediator.Send(command)).AddResourceUri(RoutesPrefix));

group.MapPut("/{id}", async Task<ApiResult> (TenantId id, UpdateTenant.Command command, ISender mediator)
group.MapPut("/{id}", async Task<ApiResult> (TenantId id, UpdateTenantCommand command, ISender mediator)
=> await mediator.Send(command with {Id = id}));

group.MapDelete("/{id}", async Task<ApiResult> (TenantId id, ISender mediator)
=> await mediator.Send(new DeleteTenant.Command(id)));
=> await mediator.Send(new DeleteTenantCommand(id)));
}
}
10 changes: 5 additions & 5 deletions application/account-management/Api/Users/UserEndpoints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,15 @@ public static void MapUserEndpoints(this IEndpointRouteBuilder routes)
var group = routes.MapGroup(RoutesPrefix);

group.MapGet("/{id}", async Task<ApiResult<UserResponseDto>> (UserId id, ISender mediator)
=> await mediator.Send(new GetUser.Query(id)));
=> await mediator.Send(new GetUserQuery(id)));

group.MapPost("/", async Task<ApiResult> (CreateUser.Command command, ISender mediator)
group.MapPost("/", async Task<ApiResult> (CreateUserCommand command, ISender mediator)
=> (await mediator.Send(command)).AddResourceUri(RoutesPrefix));

group.MapPut("/{id}", async Task<ApiResult> (UserId id, UpdateUser.Command command, ISender mediator)
=> await mediator.Send(command with {Id = id}));
group.MapPut("/{id}", async Task<ApiResult> (UserId id, UpdateUserCommand command, ISender mediator)
=> await mediator.Send(command with { Id = id }));

group.MapDelete("/{id}", async Task<ApiResult> (UserId id, ISender mediator)
=> await mediator.Send(new DeleteUser.Command(id)));
=> await mediator.Send(new DeleteUserCommand(id)));
}
}
87 changes: 42 additions & 45 deletions application/account-management/Application/Tenants/CreateTenant.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,56 +5,53 @@

namespace PlatformPlatform.AccountManagement.Application.Tenants;

public static class CreateTenant
public sealed record CreateTenantCommand(string Subdomain, string Name, string? Phone, string Email)
: ICommand, ITenantValidation, IRequest<Result<TenantId>>;

[UsedImplicitly]
public sealed class CreateTenantHandler : IRequestHandler<CreateTenantCommand, Result<TenantId>>
{
public sealed record Command(string Subdomain, string Name, string? Phone, string Email)
: ICommand, ITenantValidation, IRequest<Result<TenantId>>;
private readonly ISender _mediator;
private readonly ITenantRepository _tenantRepository;

public CreateTenantHandler(ITenantRepository tenantRepository, ISender mediator)
{
_tenantRepository = tenantRepository;
_mediator = mediator;
}

public async Task<Result<TenantId>> Handle(CreateTenantCommand command, CancellationToken cancellationToken)
{
var tenant = Tenant.Create(command.Subdomain, command.Name, command.Phone);
await _tenantRepository.AddAsync(tenant, cancellationToken);

await CreateTenantOwnerAsync(tenant.Id, command.Email, cancellationToken);
return tenant.Id;
}

[UsedImplicitly]
public sealed class Handler : IRequestHandler<Command, Result<TenantId>>
private async Task CreateTenantOwnerAsync(TenantId tenantId, string tenantOwnerEmail,
CancellationToken cancellationToken)
{
private readonly ISender _mediator;
private readonly ITenantRepository _tenantRepository;

public Handler(ITenantRepository tenantRepository, ISender mediator)
{
_tenantRepository = tenantRepository;
_mediator = mediator;
}

public async Task<Result<TenantId>> Handle(Command command, CancellationToken cancellationToken)
{
var tenant = Tenant.Create(command.Subdomain, command.Name, command.Phone);
await _tenantRepository.AddAsync(tenant, cancellationToken);

await CreateTenantOwnerAsync(tenant.Id, command.Email, cancellationToken);
return tenant.Id;
}

private async Task CreateTenantOwnerAsync(TenantId tenantId, string tenantOwnerEmail,
CancellationToken cancellationToken)
{
var createTenantOwnerUserCommand = new CreateUser.Command(tenantId, tenantOwnerEmail, UserRole.TenantOwner);
var result = await _mediator.Send(createTenantOwnerUserCommand, cancellationToken);

if (!result.IsSuccess) throw new UnreachableException($"Create Tenant Owner: {result.GetErrorSummary()}");
}
var createTenantOwnerUserCommand = new CreateUserCommand(tenantId, tenantOwnerEmail, UserRole.TenantOwner);
var result = await _mediator.Send(createTenantOwnerUserCommand, cancellationToken);

if (!result.IsSuccess) throw new UnreachableException($"Create Tenant Owner: {result.GetErrorSummary()}");
}
}

[UsedImplicitly]
public sealed class Validator : TenantValidator<Command>
[UsedImplicitly]
public sealed class CreateTenantValidator : TenantValidator<CreateTenantCommand>
{
public CreateTenantValidator(ITenantRepository tenantRepository)
{
public Validator(ITenantRepository tenantRepository)
{
RuleFor(x => x.Email).NotEmpty().SetValidator(new SharedValidations.Email());
RuleFor(x => x.Subdomain).NotEmpty();
RuleFor(x => x.Subdomain)
.Matches("^[a-z0-9]{3,30}$")
.WithMessage("Subdomain must be between 3-30 alphanumeric and lowercase characters.")
.MustAsync(async (subdomain, cancellationToken) =>
await tenantRepository.IsSubdomainFreeAsync(subdomain, cancellationToken))
.WithMessage("The subdomain is not available.")
.When(x => !string.IsNullOrEmpty(x.Subdomain));
}
RuleFor(x => x.Email).NotEmpty().SetValidator(new SharedValidations.Email());
RuleFor(x => x.Subdomain).NotEmpty();
RuleFor(x => x.Subdomain)
.Matches("^[a-z0-9]{3,30}$")
.WithMessage("Subdomain must be between 3-30 alphanumeric and lowercase characters.")
.MustAsync(async (subdomain, cancellationToken) =>
await tenantRepository.IsSubdomainFreeAsync(subdomain, cancellationToken))
.WithMessage("The subdomain is not available.")
.When(x => !string.IsNullOrEmpty(x.Subdomain));
}
}
52 changes: 23 additions & 29 deletions application/account-management/Application/Tenants/DeleteTenant.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,42 +3,36 @@

namespace PlatformPlatform.AccountManagement.Application.Tenants;

public static class DeleteTenant
public sealed record DeleteTenantCommand(TenantId Id) : ICommand, IRequest<Result>;

[UsedImplicitly]
public sealed class DeleteTenantHandler : IRequestHandler<DeleteTenantCommand, Result>
{
public sealed record Command(TenantId Id) : ICommand, IRequest<Result>;
private readonly ITenantRepository _tenantRepository;

[UsedImplicitly]
public sealed class Handler : IRequestHandler<Command, Result>
public DeleteTenantHandler(ITenantRepository tenantRepository)
{
private readonly ITenantRepository _tenantRepository;

public Handler(ITenantRepository tenantRepository)
{
_tenantRepository = tenantRepository;
}
_tenantRepository = tenantRepository;
}

public async Task<Result> Handle(Command command, CancellationToken cancellationToken)
{
var tenant = await _tenantRepository.GetByIdAsync(command.Id, cancellationToken);
if (tenant is null)
{
return Result.NotFound($"Tenant with id '{command.Id}' not found.");
}
public async Task<Result> Handle(DeleteTenantCommand command, CancellationToken cancellationToken)
{
var tenant = await _tenantRepository.GetByIdAsync(command.Id, cancellationToken);
if (tenant is null) return Result.NotFound($"Tenant with id '{command.Id}' not found.");

_tenantRepository.Remove(tenant);
return Result.Success();
}
_tenantRepository.Remove(tenant);
return Result.Success();
}
}

[UsedImplicitly]
public sealed class Validator : AbstractValidator<Command>
[UsedImplicitly]
public sealed class DeleteTenantValidator : AbstractValidator<DeleteTenantCommand>
{
public DeleteTenantValidator(IUserRepository userRepository)
{
public Validator(IUserRepository userRepository)
{
RuleFor(x => x.Id)
.MustAsync(async (tenantId, cancellationToken) =>
await userRepository.CountTenantUsersAsync(tenantId, cancellationToken) == 0)
.WithMessage("All users must be deleted before the tenant can be deleted.");
}
RuleFor(x => x.Id)
.MustAsync(async (tenantId, cancellationToken) =>
await userRepository.CountTenantUsersAsync(tenantId, cancellationToken) == 0)
.WithMessage("All users must be deleted before the tenant can be deleted.");
}
}
29 changes: 13 additions & 16 deletions application/account-management/Application/Tenants/GetTenant.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,22 @@

namespace PlatformPlatform.AccountManagement.Application.Tenants;

public static class GetTenant
public sealed record GetTenantQuery(TenantId Id) : IRequest<Result<TenantResponseDto>>;

[UsedImplicitly]
public sealed class GetTenantHandler : IRequestHandler<GetTenantQuery, Result<TenantResponseDto>>
{
public sealed record Query(TenantId Id) : IRequest<Result<TenantResponseDto>>;
private readonly ITenantRepository _tenantRepository;

[UsedImplicitly]
public sealed class Handler : IRequestHandler<Query, Result<TenantResponseDto>>
public GetTenantHandler(ITenantRepository tenantRepository)
{
private readonly ITenantRepository _tenantRepository;

public Handler(ITenantRepository tenantRepository)
{
_tenantRepository = tenantRepository;
}
_tenantRepository = tenantRepository;
}

public async Task<Result<TenantResponseDto>> Handle(Query request, CancellationToken cancellationToken)
{
var tenant = await _tenantRepository.GetByIdAsync(request.Id, cancellationToken);
return tenant?.Adapt<TenantResponseDto>()
?? Result<TenantResponseDto>.NotFound($"Tenant with id '{request.Id}' not found.");
}
public async Task<Result<TenantResponseDto>> Handle(GetTenantQuery request, CancellationToken cancellationToken)
{
var tenant = await _tenantRepository.GetByIdAsync(request.Id, cancellationToken);
return tenant?.Adapt<TenantResponseDto>()
?? Result<TenantResponseDto>.NotFound($"Tenant with id '{request.Id}' not found.");
}
}
56 changes: 25 additions & 31 deletions application/account-management/Application/Tenants/UpdateTenant.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,44 +2,38 @@

namespace PlatformPlatform.AccountManagement.Application.Tenants;

public static class UpdateTenant
public sealed record UpdateTenantCommand : ICommand, ITenantValidation, IRequest<Result>
{
public sealed record Command : ICommand, ITenantValidation, IRequest<Result>
{
[JsonIgnore] // Removes the Id from the API contract
public TenantId Id { get; init; } = null!;
[JsonIgnore] // Removes the Id from the API contract
public TenantId Id { get; init; } = null!;

public required string Name { get; init; }
public required string Name { get; init; }

public string? Phone { get; init; }
}
public string? Phone { get; init; }
}

[UsedImplicitly]
public sealed class UpdateTenantHandler : IRequestHandler<UpdateTenantCommand, Result>
{
private readonly ITenantRepository _tenantRepository;

[UsedImplicitly]
public sealed class Handler : IRequestHandler<Command, Result>
public UpdateTenantHandler(ITenantRepository tenantRepository)
{
private readonly ITenantRepository _tenantRepository;

public Handler(ITenantRepository tenantRepository)
{
_tenantRepository = tenantRepository;
}

public async Task<Result> Handle(Command command, CancellationToken cancellationToken)
{
var tenant = await _tenantRepository.GetByIdAsync(command.Id, cancellationToken);
if (tenant is null)
{
return Result.NotFound($"Tenant with id '{command.Id}' not found.");
}

tenant.Update(command.Name, command.Phone);
_tenantRepository.Update(tenant);
return Result.Success();
}
_tenantRepository = tenantRepository;
}

[UsedImplicitly]
public sealed class Validator : TenantValidator<Command>
public async Task<Result> Handle(UpdateTenantCommand command, CancellationToken cancellationToken)
{
var tenant = await _tenantRepository.GetByIdAsync(command.Id, cancellationToken);
if (tenant is null) return Result.NotFound($"Tenant with id '{command.Id}' not found.");

tenant.Update(command.Name, command.Phone);
_tenantRepository.Update(tenant);
return Result.Success();
}
}

[UsedImplicitly]
public sealed class UpdateTenantValidator : TenantValidator<UpdateTenantCommand>
{
}
63 changes: 30 additions & 33 deletions application/account-management/Application/Users/CreateUser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,46 +3,43 @@

namespace PlatformPlatform.AccountManagement.Application.Users;

public static class CreateUser
public sealed record CreateUserCommand(TenantId TenantId, string Email, UserRole UserRole)
: ICommand, IUserValidation, IRequest<Result<UserId>>;

[UsedImplicitly]
public sealed class CreateUserHandler : IRequestHandler<CreateUserCommand, Result<UserId>>
{
public sealed record Command(TenantId TenantId, string Email, UserRole UserRole)
: ICommand, IUserValidation, IRequest<Result<UserId>>;
private readonly IUserRepository _userRepository;

[UsedImplicitly]
public sealed class Handler : IRequestHandler<Command, Result<UserId>>
public CreateUserHandler(IUserRepository userRepository)
{
private readonly IUserRepository _userRepository;

public Handler(IUserRepository userRepository)
{
_userRepository = userRepository;
}
_userRepository = userRepository;
}

public async Task<Result<UserId>> Handle(Command command, CancellationToken cancellationToken)
{
var user = User.Create(command.TenantId, command.Email, command.UserRole);
await _userRepository.AddAsync(user, cancellationToken);
return user.Id;
}
public async Task<Result<UserId>> Handle(CreateUserCommand command, CancellationToken cancellationToken)
{
var user = User.Create(command.TenantId, command.Email, command.UserRole);
await _userRepository.AddAsync(user, cancellationToken);
return user.Id;
}
}

[UsedImplicitly]
public sealed class Validator : UserValidator<Command>
[UsedImplicitly]
public sealed class CreateUserValidator : UserValidator<CreateUserCommand>
{
public CreateUserValidator(IUserRepository userRepository, ITenantRepository tenantRepository)
{
public Validator(IUserRepository userRepository, ITenantRepository tenantRepository)
{
RuleFor(x => x.TenantId)
.MustAsync(async (tenantId, cancellationToken) =>
await tenantRepository.ExistsAsync(tenantId, cancellationToken))
.WithMessage(x => $"The tenant '{x.TenantId}' does not exist.")
.When(x => !string.IsNullOrEmpty(x.Email));
RuleFor(x => x.TenantId)
.MustAsync(async (tenantId, cancellationToken) =>
await tenantRepository.ExistsAsync(tenantId, cancellationToken))
.WithMessage(x => $"The tenant '{x.TenantId}' does not exist.")
.When(x => !string.IsNullOrEmpty(x.Email));

RuleFor(x => x)
.MustAsync(async (x, cancellationToken)
=> await userRepository.IsEmailFreeAsync(x.TenantId, x.Email, cancellationToken))
.WithName("Email")
.WithMessage(x => $"The email '{x.Email}' is already in use by another user on this tenant.")
.When(x => !string.IsNullOrEmpty(x.Email));
}
RuleFor(x => x)
.MustAsync(async (x, cancellationToken)
=> await userRepository.IsEmailFreeAsync(x.TenantId, x.Email, cancellationToken))
.WithName("Email")
.WithMessage(x => $"The email '{x.Email}' is already in use by another user on this tenant.")
.When(x => !string.IsNullOrEmpty(x.Email));
}
}
Loading