0
0
mirror of https://github.com/alex289/CleanArchitecture.git synced 2025-06-30 02:31:08 +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;
[ApiController]
[Authorize]
[Route("/api/v1/[controller]")]
public sealed class UserController : ApiController
{
@ -25,7 +26,6 @@ public sealed class UserController : ApiController
_userService = userService;
}
[Authorize]
[HttpGet]
[SwaggerOperation("Get a list of all users")]
[SwaggerResponse(200, "Request successful", typeof(ResponseMessage<IEnumerable<UserViewModel>>))]
@ -35,7 +35,6 @@ public sealed class UserController : ApiController
return Response(users);
}
[Authorize]
[HttpGet("{id:guid}")]
[SwaggerOperation("Get a user by id")]
[SwaggerResponse(200, "Request successful", typeof(ResponseMessage<UserViewModel>))]
@ -47,7 +46,6 @@ public sealed class UserController : ApiController
return Response(user);
}
[Authorize]
[HttpGet("me")]
[SwaggerOperation("Get the current active user")]
[SwaggerResponse(200, "Request successful", typeof(ResponseMessage<UserViewModel>))]
@ -58,6 +56,7 @@ public sealed class UserController : ApiController
}
[HttpPost]
[AllowAnonymous]
[SwaggerOperation("Create a new user")]
[SwaggerResponse(200, "Request successful", typeof(ResponseMessage<Guid>))]
public async Task<IActionResult> CreateUserAsync([FromBody] CreateUserViewModel viewModel)
@ -66,7 +65,6 @@ public sealed class UserController : ApiController
return Response(userId);
}
[Authorize]
[HttpDelete("{id:guid}")]
[SwaggerOperation("Delete a user")]
[SwaggerResponse(200, "Request successful", typeof(ResponseMessage<Guid>))]
@ -76,7 +74,6 @@ public sealed class UserController : ApiController
return Response(id);
}
[Authorize]
[HttpPut]
[SwaggerOperation("Update a user")]
[SwaggerResponse(200, "Request successful", typeof(ResponseMessage<UpdateUserViewModel>))]
@ -86,7 +83,6 @@ public sealed class UserController : ApiController
return Response(viewModel);
}
[Authorize]
[HttpPost("changePassword")]
[SwaggerOperation("Change a password for the current active user")]
[SwaggerResponse(200, "Request successful", typeof(ResponseMessage<ChangePasswordViewModel>))]
@ -97,6 +93,7 @@ public sealed class UserController : ApiController
}
[HttpPost("login")]
[AllowAnonymous]
[SwaggerOperation("Get a signed token for a user")]
[SwaggerResponse(200, "Request successful", typeof(ResponseMessage<string>))]
public async Task<IActionResult> LoginUserAsync([FromBody] LoginUserViewModel viewModel)

View File

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

View File

@ -1,8 +1,11 @@
using System.Collections.Generic;
using CleanArchitecture.Application.Interfaces;
using CleanArchitecture.Application.Queries.Tenants.GetAll;
using CleanArchitecture.Application.Queries.Tenants.GetTenantById;
using CleanArchitecture.Application.Queries.Users.GetAll;
using CleanArchitecture.Application.Queries.Users.GetUserById;
using CleanArchitecture.Application.Services;
using CleanArchitecture.Application.ViewModels.Tenants;
using CleanArchitecture.Application.ViewModels.Users;
using MediatR;
using Microsoft.Extensions.DependencyInjection;
@ -14,15 +17,21 @@ public static class ServiceCollectionExtension
public static IServiceCollection AddServices(this IServiceCollection services)
{
services.AddScoped<IUserService, UserService>();
services.AddScoped<ITenantService, TenantService>();
return services;
}
public static IServiceCollection AddQueryHandlers(this IServiceCollection services)
{
// User
services.AddScoped<IRequestHandler<GetUserByIdQuery, UserViewModel?>, GetUserByIdQueryHandler>();
services.AddScoped<IRequestHandler<GetAllUsersQuery, IEnumerable<UserViewModel>>, GetAllUsersQueryHandler>();
// Tenant
services.AddScoped<IRequestHandler<GetTenantByIdQuery, TenantViewModel?>, GetTenantByIdQueryHandler>();
services.AddScoped<IRequestHandler<GetAllTenantsQuery, IEnumerable<TenantViewModel>>, GetAllTenantsQueryHandler>();
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.Deleted == request.IsDeleted);
if (user == null)
if (user is null)
{
await _bus.RaiseEventAsync(
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;
}
public override async Task<GetByIdsResult> GetByIds(
GetByIdsRequest request,
public override async Task<GetUsersByIdsResult> GetByIds(
GetUsersByIdsRequest request,
ServerCallContext context)
{
var idsAsGuids = new List<Guid>(request.Ids.Count);
@ -45,7 +45,7 @@ public sealed class UsersApiImplementation : UsersApi.UsersApiBase
})
.ToListAsync();
var result = new GetByIdsResult();
var result = new GetUsersByIdsResult();
result.Users.AddRange(users);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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
{
private readonly ChangePasswordCommandValidation _validation = new();
private static readonly ChangePasswordCommandValidation s_validation = new();
public ChangePasswordCommand(string password, string newPassword) : base(Guid.NewGuid())
{
@ -17,7 +17,7 @@ public sealed class ChangePasswordCommand : CommandBase
public override bool IsValid()
{
ValidationResult = _validation.Validate(this);
ValidationResult = s_validation.Validate(this);
return ValidationResult.IsValid;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,26 +2,40 @@ namespace CleanArchitecture.Domain.Errors;
public static class DomainErrorCodes
{
// User Validation
public const string UserEmptyId = "USER_EMPTY_ID";
public const string UserEmptyFirstName = "USER_EMPTY_FIRST_NAME";
public const string UserEmptyLastName = "USER_EMPTY_LAST_NAME";
public const string UserEmailExceedsMaxLength = "USER_EMAIL_EXCEEDS_MAX_LENGTH";
public const string UserFirstNameExceedsMaxLength = "USER_FIRST_NAME_EXCEEDS_MAX_LENGTH";
public const string UserLastNameExceedsMaxLength = "USER_LAST_NAME_EXCEEDS_MAX_LENGTH";
public const string UserInvalidEmail = "USER_INVALID_EMAIL";
public const string UserInvalidRole = "USER_INVALID_ROLE";
public static class User
{
// User Validation
public const string UserEmptyId = "USER_EMPTY_ID";
public const string UserEmptyFirstName = "USER_EMPTY_FIRST_NAME";
public const string UserEmptyLastName = "USER_EMPTY_LAST_NAME";
public const string UserEmailExceedsMaxLength = "USER_EMAIL_EXCEEDS_MAX_LENGTH";
public const string UserFirstNameExceedsMaxLength = "USER_FIRST_NAME_EXCEEDS_MAX_LENGTH";
public const string UserLastNameExceedsMaxLength = "USER_LAST_NAME_EXCEEDS_MAX_LENGTH";
public const string UserInvalidEmail = "USER_INVALID_EMAIL";
public const string UserInvalidRole = "USER_INVALID_ROLE";
// User Password Validation
public const string UserEmptyPassword = "USER_PASSWORD_MAY_NOT_BE_EMPTY";
public const string UserShortPassword = "USER_PASSWORD_MAY_NOT_BE_SHORTER_THAN_6_CHARACTERS";
public const string UserLongPassword = "USER_PASSWORD_MAY_NOT_BE_LONGER_THAN_50_CHARACTERS";
public const string UserUppercaseLetterPassword = "USER_PASSWORD_MUST_CONTAIN_A_UPPERCASE_LETTER";
public const string UserLowercaseLetterPassword = "USER_PASSWORD_MUST_CONTAIN_A_LOWERCASE_LETTER";
public const string UserNumberPassword = "USER_PASSWORD_MUST_CONTAIN_A_NUMBER";
public const string UserSpecialCharPassword = "USER_PASSWORD_MUST_CONTAIN_A_SPECIAL_CHARACTER";
// User Password Validation
public const string UserEmptyPassword = "USER_PASSWORD_MAY_NOT_BE_EMPTY";
public const string UserShortPassword = "USER_PASSWORD_MAY_NOT_BE_SHORTER_THAN_6_CHARACTERS";
public const string UserLongPassword = "USER_PASSWORD_MAY_NOT_BE_LONGER_THAN_50_CHARACTERS";
public const string UserUppercaseLetterPassword = "USER_PASSWORD_MUST_CONTAIN_A_UPPERCASE_LETTER";
public const string UserLowercaseLetterPassword = "USER_PASSWORD_MUST_CONTAIN_A_LOWERCASE_LETTER";
public const string UserNumberPassword = "USER_PASSWORD_MUST_CONTAIN_A_NUMBER";
public const string UserSpecialCharPassword = "USER_PASSWORD_MUST_CONTAIN_A_SPECIAL_CHARACTER";
// User
public const string UserAlreadyExists = "USER_ALREADY_EXISTS";
public const string UserPasswordIncorrect = "USER_PASSWORD_INCORRECT";
// General
public const string UserAlreadyExists = "USER_ALREADY_EXISTS";
public const string UserPasswordIncorrect = "USER_PASSWORD_INCORRECT";
}
public static class Tenant
{
// Tenant Validation
public const string TenantEmptyId = "TENANT_EMPTY_ID";
public const string TenantEmptyName = "TENANT_EMPTY_NAME";
public const string TenantNameExceedsMaxLength = "TENANT_NAME_EXCEEDS_MAX_LENGTH";
// General
public const string TenantAlreadyExists = "TENANT_ALREADY_EXISTS";
}
}

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)
{
UserId = userId;
}
public Guid UserId { get; }
}

View File

@ -7,8 +7,5 @@ public sealed class UserCreatedEvent : DomainEvent
{
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)
{
UserId = userId;
}
public Guid UserId { get; }
}

View File

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

View File

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

View File

@ -22,4 +22,5 @@ public interface IRepository<TEntity> : IDisposable where TEntity : Entity
Task<bool> ExistsAsync(Guid id);
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;
if (httpContextAccessor?.HttpContext == null ||
if (httpContextAccessor?.HttpContext is null ||
!httpContextAccessor.HttpContext.Request.Headers.TryGetValue("X-CLEAN-ARCHITECTURE-CORRELATION-ID", out var id))
{
_correlationId = $"internal - {Guid.NewGuid()}";

View File

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

View File

@ -55,9 +55,9 @@ public class BaseRepository<TEntity> : IRepository<TEntity> where TEntity : Enti
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)
@ -72,6 +72,20 @@ public class BaseRepository<TEntity> : IRepository<TEntity> where TEntity : Enti
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()
{

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
{
var descriptor = services.SingleOrDefault(d => d.ServiceType == typeof(DbContextOptions<TContext>));
if (descriptor != null)
if (descriptor is not null)
services.Remove(descriptor);
services.AddScoped(p =>

View File

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

View File

@ -5,14 +5,11 @@
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<None Remove="Users\Models.proto" />
<None Remove="Users\UsersApi.proto" />
</ItemGroup>
<ItemGroup>
<Protobuf Include="Users\Models.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>

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;
}
message GetByIdsResult {
message GetUsersByIdsResult {
repeated GrpcUser users = 1;
}
message GetByIdsRequest {
message GetUsersByIdsRequest {
repeated string ids = 1;
}

View File

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

View File

@ -5,4 +5,5 @@
<Nullable>enable</Nullable>
</PropertyGroup>
</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.Add("Not a guid");

View File

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

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)
{
var request = new GetByIdsRequest();
var request = new GetUsersByIdsRequest();
request.Ids.AddRange(ids.Select(id => id.ToString()));

View File

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

View File

@ -5,4 +5,5 @@ namespace CleanArchitecture.gRPC;
public interface ICleanArchitecture
{
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);
}