Skip to content

Commit

Permalink
Remove static "parent" classes from commands and queries, use files a…
Browse files Browse the repository at this point in the history
…s containers instead
  • Loading branch information
tjementum committed Nov 12, 2023
1 parent 4e3b61b commit 2800ac3
Show file tree
Hide file tree
Showing 14 changed files with 211 additions and 247 deletions.
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

0 comments on commit 2800ac3

Please sign in to comment.