From d72785e37ef548fa8bc8cba2f9dc3e3fe5ced29b Mon Sep 17 00:00:00 2001 From: Andrija Stevanovic Date: Wed, 11 Feb 2026 19:10:55 +0100 Subject: [PATCH] implement --- .../AddUserToWhiteboardCommand.cs | 11 ++++ .../AddUserToWhiteboardCommandErrors.cs | 12 ++++ .../AddUserToWhiteboardCommandHandler.cs | 65 +++++++++++++++++++ .../AipsCore/Domain/Abstract/DomainModel.cs | 5 ++ .../Abstract/Validation/AbstractErrors.cs | 38 +++++++++++ .../Common/Validation/ValidationException.cs | 5 ++ .../Domain/Common/ValueObjects/DomainId.cs | 5 ++ .../Models/User/Validation/UserErrors.cs | 9 +++ .../Whiteboard/Validation/WhiteboardErrors.cs | 18 +++++ .../Models/Whiteboard/Whiteboard.AddUser.cs | 42 ++++++++++++ .../Domain/Models/Whiteboard/Whiteboard.cs | 2 +- .../IWhiteboardMembershipRepository.cs | 4 +- .../WhiteboardMembershipEditingEnabled.cs | 6 ++ .../WhiteboardMembershipRepository.cs | 16 +++++ 14 files changed, 236 insertions(+), 2 deletions(-) create mode 100644 dotnet/AipsCore/Application/Models/Whiteboard/Command/AddUserToWhiteboard/AddUserToWhiteboardCommand.cs create mode 100644 dotnet/AipsCore/Application/Models/Whiteboard/Command/AddUserToWhiteboard/AddUserToWhiteboardCommandErrors.cs create mode 100644 dotnet/AipsCore/Application/Models/Whiteboard/Command/AddUserToWhiteboard/AddUserToWhiteboardCommandHandler.cs create mode 100644 dotnet/AipsCore/Domain/Abstract/Validation/AbstractErrors.cs create mode 100644 dotnet/AipsCore/Domain/Models/User/Validation/UserErrors.cs create mode 100644 dotnet/AipsCore/Domain/Models/Whiteboard/Validation/WhiteboardErrors.cs create mode 100644 dotnet/AipsCore/Domain/Models/Whiteboard/Whiteboard.AddUser.cs diff --git a/dotnet/AipsCore/Application/Models/Whiteboard/Command/AddUserToWhiteboard/AddUserToWhiteboardCommand.cs b/dotnet/AipsCore/Application/Models/Whiteboard/Command/AddUserToWhiteboard/AddUserToWhiteboardCommand.cs new file mode 100644 index 0000000..04f3faf --- /dev/null +++ b/dotnet/AipsCore/Application/Models/Whiteboard/Command/AddUserToWhiteboard/AddUserToWhiteboardCommand.cs @@ -0,0 +1,11 @@ +using AipsCore.Application.Abstract.Command; +using AipsCore.Domain.Models.User.ValueObjects; +using AipsCore.Domain.Models.Whiteboard.ValueObjects; +using AipsCore.Domain.Models.WhiteboardMembership.ValueObjects; + +namespace AipsCore.Application.Models.Whiteboard.Command.AddUserToWhiteboard; + +public record AddUserToWhiteboardCommand( + string UserId, + string WhiteboardId) + : ICommand; \ No newline at end of file diff --git a/dotnet/AipsCore/Application/Models/Whiteboard/Command/AddUserToWhiteboard/AddUserToWhiteboardCommandErrors.cs b/dotnet/AipsCore/Application/Models/Whiteboard/Command/AddUserToWhiteboard/AddUserToWhiteboardCommandErrors.cs new file mode 100644 index 0000000..b530c3e --- /dev/null +++ b/dotnet/AipsCore/Application/Models/Whiteboard/Command/AddUserToWhiteboard/AddUserToWhiteboardCommandErrors.cs @@ -0,0 +1,12 @@ +using AipsCore.Domain.Common.Validation; +using AipsCore.Domain.Models.Whiteboard.ValueObjects; + +namespace AipsCore.Application.Models.Whiteboard.Command.AddUserToWhiteboard; + +public static class AddUserToWhiteboardCommandErrors +{ + public static ValidationError WhiteboardDoesNotExist(WhiteboardId whiteboardId) + => new ValidationError( + Code: "whiteboard_not_exists", + Message: $"Whiteboard with id '{whiteboardId}' does not exist."); +} \ No newline at end of file diff --git a/dotnet/AipsCore/Application/Models/Whiteboard/Command/AddUserToWhiteboard/AddUserToWhiteboardCommandHandler.cs b/dotnet/AipsCore/Application/Models/Whiteboard/Command/AddUserToWhiteboard/AddUserToWhiteboardCommandHandler.cs new file mode 100644 index 0000000..645abf3 --- /dev/null +++ b/dotnet/AipsCore/Application/Models/Whiteboard/Command/AddUserToWhiteboard/AddUserToWhiteboardCommandHandler.cs @@ -0,0 +1,65 @@ +using AipsCore.Application.Abstract; +using AipsCore.Application.Abstract.Command; +using AipsCore.Domain.Abstract; +using AipsCore.Domain.Common.Validation; +using AipsCore.Domain.Models.User.External; +using AipsCore.Domain.Models.User.Validation; +using AipsCore.Domain.Models.User.ValueObjects; +using AipsCore.Domain.Models.Whiteboard.External; +using AipsCore.Domain.Models.Whiteboard.Validation; +using AipsCore.Domain.Models.Whiteboard.ValueObjects; +using AipsCore.Domain.Models.WhiteboardMembership.External; + +namespace AipsCore.Application.Models.Whiteboard.Command.AddUserToWhiteboard; + +public class AddUserToWhiteboardCommandHandler + : ICommandHandler +{ + private readonly IWhiteboardRepository _whiteboardRepository; + private readonly IWhiteboardMembershipRepository _whiteboardMembershipRepository; + private readonly IUserRepository _userRepository; + private readonly IUnitOfWork _unitOfWork; + private readonly IDispatcher _dispatcher; + + public AddUserToWhiteboardCommandHandler( + IWhiteboardRepository whiteboardRepository, + IWhiteboardMembershipRepository whiteboardMembershipRepository, + IUserRepository userRepository, + IUnitOfWork unitOfWork, + IDispatcher dispatcher) + { + _whiteboardRepository = whiteboardRepository; + _whiteboardMembershipRepository = whiteboardMembershipRepository; + _userRepository = userRepository; + _unitOfWork = unitOfWork; + _dispatcher = dispatcher; + } + + private Domain.Models.Whiteboard.Whiteboard? _whiteboard; + private Domain.Models.User.User? _user; + + public async Task Handle(AddUserToWhiteboardCommand command, CancellationToken cancellationToken = default) + { + _whiteboard = await _whiteboardRepository.GetByIdAsync(new WhiteboardId(command.WhiteboardId), cancellationToken); + _user = await _userRepository.GetByIdAsync(new UserId(command.UserId), cancellationToken); + + Validate(command); + + await _whiteboard!.AddUserAsync(_user!, _whiteboardMembershipRepository, cancellationToken); + + await _unitOfWork.SaveChangesAsync(cancellationToken); + } + + private void Validate(AddUserToWhiteboardCommand command) + { + if (_whiteboard is null) + { + throw new ValidationException(WhiteboardErrors.NotFound(new WhiteboardId(command.WhiteboardId))); + } + + if (_user is null) + { + throw new ValidationException(UserErrors.NotFound(new UserId(command.UserId))); + } + } +} \ No newline at end of file diff --git a/dotnet/AipsCore/Domain/Abstract/DomainModel.cs b/dotnet/AipsCore/Domain/Abstract/DomainModel.cs index 895b030..3944912 100644 --- a/dotnet/AipsCore/Domain/Abstract/DomainModel.cs +++ b/dotnet/AipsCore/Domain/Abstract/DomainModel.cs @@ -18,6 +18,11 @@ public abstract class DomainModel where TId : DomainId Id = id; } + public string GetModelName() + { + return this.GetType().Name; + } + public IReadOnlyList GetDomainEvents() => _domainEvents.ToList(); public void ClearDomainEvents() => _domainEvents.Clear(); diff --git a/dotnet/AipsCore/Domain/Abstract/Validation/AbstractErrors.cs b/dotnet/AipsCore/Domain/Abstract/Validation/AbstractErrors.cs new file mode 100644 index 0000000..738236d --- /dev/null +++ b/dotnet/AipsCore/Domain/Abstract/Validation/AbstractErrors.cs @@ -0,0 +1,38 @@ +using AipsCore.Domain.Common.Validation; +using AipsCore.Domain.Common.ValueObjects; +using AipsCore.Domain.Models.Whiteboard.ValueObjects; + +namespace AipsCore.Domain.Abstract.Validation; + +public abstract class AbstractErrors +where TModel : DomainModel +where TId : DomainId +{ + public static string GetModelName() + { + return typeof(TModel).Name; + } + + public static string Prefix() + { + return GetModelName().ToLower(); + } + + protected static string GetFullCode(string code) + { + return $"{Prefix()}_{code}"; + } + + protected static ValidationError CreateValidationError(string code, string errorMessage) + { + return new ValidationError(GetFullCode(code), errorMessage); + } + + public static ValidationError NotFound(TId id) + { + const string code = "not_found"; + string message = $"{GetModelName()} with id '{id}' was not found!"; + + return CreateValidationError(code,message); + } +} \ No newline at end of file diff --git a/dotnet/AipsCore/Domain/Common/Validation/ValidationException.cs b/dotnet/AipsCore/Domain/Common/Validation/ValidationException.cs index 6378716..ad31376 100644 --- a/dotnet/AipsCore/Domain/Common/Validation/ValidationException.cs +++ b/dotnet/AipsCore/Domain/Common/Validation/ValidationException.cs @@ -10,4 +10,9 @@ public class ValidationException : Exception { ValidationErrors = validationErrors; } + + public ValidationException(ValidationError validationError) + : this([validationError]) + { + } } \ No newline at end of file diff --git a/dotnet/AipsCore/Domain/Common/ValueObjects/DomainId.cs b/dotnet/AipsCore/Domain/Common/ValueObjects/DomainId.cs index c638377..9f60074 100644 --- a/dotnet/AipsCore/Domain/Common/ValueObjects/DomainId.cs +++ b/dotnet/AipsCore/Domain/Common/ValueObjects/DomainId.cs @@ -22,4 +22,9 @@ public record DomainId : AbstractValueObject new MinLengthRule(IdValue, 5) ]; } + + public override string ToString() + { + return IdValue; + } } \ No newline at end of file diff --git a/dotnet/AipsCore/Domain/Models/User/Validation/UserErrors.cs b/dotnet/AipsCore/Domain/Models/User/Validation/UserErrors.cs new file mode 100644 index 0000000..558d7a6 --- /dev/null +++ b/dotnet/AipsCore/Domain/Models/User/Validation/UserErrors.cs @@ -0,0 +1,9 @@ +using AipsCore.Domain.Abstract.Validation; +using AipsCore.Domain.Models.User.ValueObjects; + +namespace AipsCore.Domain.Models.User.Validation; + +public class UserErrors : AbstractErrors +{ + +} \ No newline at end of file diff --git a/dotnet/AipsCore/Domain/Models/Whiteboard/Validation/WhiteboardErrors.cs b/dotnet/AipsCore/Domain/Models/Whiteboard/Validation/WhiteboardErrors.cs new file mode 100644 index 0000000..b06bafc --- /dev/null +++ b/dotnet/AipsCore/Domain/Models/Whiteboard/Validation/WhiteboardErrors.cs @@ -0,0 +1,18 @@ +using AipsCore.Domain.Abstract.Validation; +using AipsCore.Domain.Common.Validation; +using AipsCore.Domain.Models.User.ValueObjects; +using AipsCore.Domain.Models.Whiteboard.ValueObjects; + +namespace AipsCore.Domain.Models.Whiteboard.Validation; + +public class WhiteboardErrors : AbstractErrors +{ + public static ValidationError UserAlreadyAdded(UserId userId) + { + string code = "user_already_added"; + string message = $"User with id '{userId}' already added to this whiteboard"; + + return CreateValidationError(code, message); + } + +} \ No newline at end of file diff --git a/dotnet/AipsCore/Domain/Models/Whiteboard/Whiteboard.AddUser.cs b/dotnet/AipsCore/Domain/Models/Whiteboard/Whiteboard.AddUser.cs new file mode 100644 index 0000000..8cd8d3a --- /dev/null +++ b/dotnet/AipsCore/Domain/Models/Whiteboard/Whiteboard.AddUser.cs @@ -0,0 +1,42 @@ +using System.Runtime.InteropServices.Swift; +using AipsCore.Domain.Abstract; +using AipsCore.Domain.Common.Validation; +using AipsCore.Domain.Models.Whiteboard.Enums; +using AipsCore.Domain.Models.Whiteboard.Validation; +using AipsCore.Domain.Models.Whiteboard.ValueObjects; +using AipsCore.Domain.Models.WhiteboardMembership.External; + +namespace AipsCore.Domain.Models.Whiteboard; + +public partial class Whiteboard : DomainModel +{ + public async Task AddUserAsync( + User.User user, + IWhiteboardMembershipRepository membershipRepository, + CancellationToken cancellationToken = default) + { + var membership + = await membershipRepository.GetByWhiteboardAndUserAsync(this.Id, user.Id, cancellationToken); + + if (membership is not null) + { + throw new ValidationException(WhiteboardErrors.UserAlreadyAdded(user.Id)); + } + + membership = WhiteboardMembership.WhiteboardMembership.Create( + this.Id.ToString(), + user.Id.ToString(), + false, + false, + this.GetCanJoin(), + DateTime.Now + ); + + await membershipRepository.AddAsync(membership, cancellationToken); + } + + private bool GetCanJoin() + { + return this.JoinPolicy == WhiteboardJoinPolicy.FreeToJoin; + } +} diff --git a/dotnet/AipsCore/Domain/Models/Whiteboard/Whiteboard.cs b/dotnet/AipsCore/Domain/Models/Whiteboard/Whiteboard.cs index b774510..c4089d4 100644 --- a/dotnet/AipsCore/Domain/Models/Whiteboard/Whiteboard.cs +++ b/dotnet/AipsCore/Domain/Models/Whiteboard/Whiteboard.cs @@ -5,7 +5,7 @@ using AipsCore.Domain.Models.Whiteboard.ValueObjects; namespace AipsCore.Domain.Models.Whiteboard; -public class Whiteboard : DomainModel +public partial class Whiteboard : DomainModel { public UserId WhiteboardOwnerId { get; private set; } public WhiteboardCode Code { get; private set; } diff --git a/dotnet/AipsCore/Domain/Models/WhiteboardMembership/External/IWhiteboardMembershipRepository.cs b/dotnet/AipsCore/Domain/Models/WhiteboardMembership/External/IWhiteboardMembershipRepository.cs index f4641f3..24e4e5c 100644 --- a/dotnet/AipsCore/Domain/Models/WhiteboardMembership/External/IWhiteboardMembershipRepository.cs +++ b/dotnet/AipsCore/Domain/Models/WhiteboardMembership/External/IWhiteboardMembershipRepository.cs @@ -1,9 +1,11 @@ using AipsCore.Domain.Abstract; +using AipsCore.Domain.Models.User.ValueObjects; +using AipsCore.Domain.Models.Whiteboard.ValueObjects; using AipsCore.Domain.Models.WhiteboardMembership.ValueObjects; namespace AipsCore.Domain.Models.WhiteboardMembership.External; public interface IWhiteboardMembershipRepository : IAbstractRepository { - + Task GetByWhiteboardAndUserAsync(WhiteboardId whiteboardId, UserId userId, CancellationToken cancellationToken = default); } \ No newline at end of file diff --git a/dotnet/AipsCore/Domain/Models/WhiteboardMembership/ValueObjects/WhiteboardMembershipEditingEnabled.cs b/dotnet/AipsCore/Domain/Models/WhiteboardMembership/ValueObjects/WhiteboardMembershipEditingEnabled.cs index da674c1..61ea078 100644 --- a/dotnet/AipsCore/Domain/Models/WhiteboardMembership/ValueObjects/WhiteboardMembershipEditingEnabled.cs +++ b/dotnet/AipsCore/Domain/Models/WhiteboardMembership/ValueObjects/WhiteboardMembershipEditingEnabled.cs @@ -17,4 +17,10 @@ public record WhiteboardMembershipEditingEnabled : AbstractValueObject { return []; } + + public static WhiteboardMembershipEditingEnabled Enabled + => new WhiteboardMembershipEditingEnabled(true); + + public static WhiteboardMembershipEditingEnabled Disabled + => new WhiteboardMembershipEditingEnabled(false); } \ No newline at end of file diff --git a/dotnet/AipsCore/Infrastructure/Persistence/WhiteboardMembership/WhiteboardMembershipRepository.cs b/dotnet/AipsCore/Infrastructure/Persistence/WhiteboardMembership/WhiteboardMembershipRepository.cs index 89e97aa..58c7952 100644 --- a/dotnet/AipsCore/Infrastructure/Persistence/WhiteboardMembership/WhiteboardMembershipRepository.cs +++ b/dotnet/AipsCore/Infrastructure/Persistence/WhiteboardMembership/WhiteboardMembershipRepository.cs @@ -1,7 +1,10 @@ +using AipsCore.Domain.Models.User.ValueObjects; +using AipsCore.Domain.Models.Whiteboard.ValueObjects; using AipsCore.Domain.Models.WhiteboardMembership.External; using AipsCore.Domain.Models.WhiteboardMembership.ValueObjects; using AipsCore.Infrastructure.Persistence.Abstract; using AipsCore.Infrastructure.Persistence.Db; +using Microsoft.EntityFrameworkCore; namespace AipsCore.Infrastructure.Persistence.WhiteboardMembership; @@ -49,4 +52,17 @@ public class WhiteboardMembershipRepository entity.CanJoin = model.CanJoin.CanJoinValue; entity.LastInteractedAt = model.LastInteractedAt.LastInteractedAtValue; } + + public async Task GetByWhiteboardAndUserAsync(WhiteboardId whiteboardId, UserId userId, CancellationToken cancellationToken = default) + { + var whiteboardMembership = await Context.WhiteboardMemberships + .FirstOrDefaultAsync((entity) => + entity.WhiteboardId.ToString() == whiteboardId.ToString() && + entity.UserId.ToString() == userId.ToString(), + cancellationToken); + + if (whiteboardMembership is null) return null; + + return MapToModel(whiteboardMembership); + } } \ No newline at end of file