diff --git a/dotnet/AipsCore/AipsCore.csproj b/dotnet/AipsCore/AipsCore.csproj
index 0124700..d062ba1 100644
--- a/dotnet/AipsCore/AipsCore.csproj
+++ b/dotnet/AipsCore/AipsCore.csproj
@@ -17,6 +17,7 @@
+
diff --git a/dotnet/AipsCore/Application/Models/WhiteboardMembership/Command/CreateWhiteboardMembership/CreateWhiteboardMembershipCommand.cs b/dotnet/AipsCore/Application/Models/WhiteboardMembership/Command/CreateWhiteboardMembership/CreateWhiteboardMembershipCommand.cs
new file mode 100644
index 0000000..00b0665
--- /dev/null
+++ b/dotnet/AipsCore/Application/Models/WhiteboardMembership/Command/CreateWhiteboardMembership/CreateWhiteboardMembershipCommand.cs
@@ -0,0 +1,13 @@
+using AipsCore.Application.Abstract.Command;
+using AipsCore.Domain.Models.WhiteboardMembership.ValueObjects;
+
+namespace AipsCore.Application.Models.WhiteboardMembership.Command.CreateWhiteboardMembership;
+
+public record CreateWhiteboardMembershipCommand(
+ string WhiteboardId,
+ string UserId,
+ bool IsBanned,
+ bool EditingEnabled,
+ bool CanJoin,
+ DateTime LastInteractedAt)
+ : ICommand;
\ No newline at end of file
diff --git a/dotnet/AipsCore/Application/Models/WhiteboardMembership/Command/CreateWhiteboardMembership/CreateWhiteboardMembershipCommandHandler.cs b/dotnet/AipsCore/Application/Models/WhiteboardMembership/Command/CreateWhiteboardMembership/CreateWhiteboardMembershipCommandHandler.cs
new file mode 100644
index 0000000..127419f
--- /dev/null
+++ b/dotnet/AipsCore/Application/Models/WhiteboardMembership/Command/CreateWhiteboardMembership/CreateWhiteboardMembershipCommandHandler.cs
@@ -0,0 +1,34 @@
+using AipsCore.Application.Abstract.Command;
+using AipsCore.Domain.Abstract;
+using AipsCore.Domain.Models.WhiteboardMembership.External;
+using AipsCore.Domain.Models.WhiteboardMembership.ValueObjects;
+
+namespace AipsCore.Application.Models.WhiteboardMembership.Command.CreateWhiteboardMembership;
+
+public class CreateWhiteboardMembershipCommandHandler : ICommandHandler
+{
+ private readonly IWhiteboardMembershipRepository _whiteboardMembershipRepository;
+ private readonly IUnitOfWork _unitOfWork;
+
+ public CreateWhiteboardMembershipCommandHandler(IWhiteboardMembershipRepository whiteboardMembershipRepository, IUnitOfWork unitOfWork)
+ {
+ _whiteboardMembershipRepository = whiteboardMembershipRepository;
+ _unitOfWork = unitOfWork;
+ }
+
+ public async Task Handle(CreateWhiteboardMembershipCommand command, CancellationToken cancellationToken = default)
+ {
+ var whiteboardMembership = Domain.Models.WhiteboardMembership.WhiteboardMembership.Create(
+ command.WhiteboardId,
+ command.UserId,
+ command.IsBanned,
+ command.EditingEnabled,
+ command.CanJoin,
+ command.LastInteractedAt);
+
+ await _whiteboardMembershipRepository.Save(whiteboardMembership, cancellationToken);
+ await _unitOfWork.SaveChangesAsync(cancellationToken);
+
+ return whiteboardMembership.Id;
+ }
+}
\ No newline at end of file
diff --git a/dotnet/AipsCore/Domain/Common/Validation/Rules/DateInFutureRule.cs b/dotnet/AipsCore/Domain/Common/Validation/Rules/DateInFutureRule.cs
new file mode 100644
index 0000000..cf48254
--- /dev/null
+++ b/dotnet/AipsCore/Domain/Common/Validation/Rules/DateInFutureRule.cs
@@ -0,0 +1,22 @@
+using AipsCore.Domain.Abstract.Rule;
+
+namespace AipsCore.Domain.Common.Validation.Rules;
+
+public class DateInFutureRule : AbstractRule
+{
+ private readonly DateTime _date;
+ private readonly DateTime _now;
+
+ public DateInFutureRule(DateTime date)
+ {
+ _date = date;
+ _now = DateTime.Now;
+ }
+
+ protected override string ErrorCode => "date_in_future";
+ protected override string ErrorMessage => "Date must be in the future";
+ public override bool Validate()
+ {
+ return _date > _now;
+ }
+}
\ No newline at end of file
diff --git a/dotnet/AipsCore/Domain/Common/Validation/Rules/DateInPastRule.cs b/dotnet/AipsCore/Domain/Common/Validation/Rules/DateInPastRule.cs
new file mode 100644
index 0000000..2120aa6
--- /dev/null
+++ b/dotnet/AipsCore/Domain/Common/Validation/Rules/DateInPastRule.cs
@@ -0,0 +1,22 @@
+using AipsCore.Domain.Abstract.Rule;
+
+namespace AipsCore.Domain.Common.Validation.Rules;
+
+public class DateInPastRule : AbstractRule
+{
+ private readonly DateTime _date;
+ private readonly DateTime _now;
+
+ public DateInPastRule(DateTime date)
+ {
+ _date = date;
+ _now = DateTime.Now;
+ }
+
+ protected override string ErrorCode => "date_in_past";
+ protected override string ErrorMessage => "Date must be in the past";
+ public override bool Validate()
+ {
+ return _date < _now;
+ }
+}
\ No newline at end of file
diff --git a/dotnet/AipsCore/Domain/Models/WhiteboardMembership/External/IWhiteboardMembershipRepository.cs b/dotnet/AipsCore/Domain/Models/WhiteboardMembership/External/IWhiteboardMembershipRepository.cs
new file mode 100644
index 0000000..2d70762
--- /dev/null
+++ b/dotnet/AipsCore/Domain/Models/WhiteboardMembership/External/IWhiteboardMembershipRepository.cs
@@ -0,0 +1,9 @@
+using AipsCore.Domain.Models.WhiteboardMembership.ValueObjects;
+
+namespace AipsCore.Domain.Models.WhiteboardMembership.External;
+
+public interface IWhiteboardMembershipRepository
+{
+ Task Get(WhiteboardMembershipId whiteboardMembershipId, CancellationToken cancellationToken = default);
+ Task Save(WhiteboardMembership whiteboardMembership, CancellationToken cancellationToken = default);
+}
\ No newline at end of file
diff --git a/dotnet/AipsCore/Domain/Models/WhiteboardMembership/ValueObjects/WhiteboardMembershipCanJoin.cs b/dotnet/AipsCore/Domain/Models/WhiteboardMembership/ValueObjects/WhiteboardMembershipCanJoin.cs
new file mode 100644
index 0000000..48591ad
--- /dev/null
+++ b/dotnet/AipsCore/Domain/Models/WhiteboardMembership/ValueObjects/WhiteboardMembershipCanJoin.cs
@@ -0,0 +1,20 @@
+using AipsCore.Domain.Abstract.Rule;
+using AipsCore.Domain.Abstract.ValueObject;
+
+namespace AipsCore.Domain.Models.WhiteboardMembership.ValueObjects;
+
+public record WhiteboardMembershipCanJoin : AbstractValueObject
+{
+ public bool CanJoinValue { get; init; }
+
+ public WhiteboardMembershipCanJoin(bool CanJoinValue)
+ {
+ this.CanJoinValue = CanJoinValue;
+ Validate();
+ }
+
+ protected override ICollection GetValidationRules()
+ {
+ return [];
+ }
+}
\ 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
new file mode 100644
index 0000000..da674c1
--- /dev/null
+++ b/dotnet/AipsCore/Domain/Models/WhiteboardMembership/ValueObjects/WhiteboardMembershipEditingEnabled.cs
@@ -0,0 +1,20 @@
+using AipsCore.Domain.Abstract.Rule;
+using AipsCore.Domain.Abstract.ValueObject;
+
+namespace AipsCore.Domain.Models.WhiteboardMembership.ValueObjects;
+
+public record WhiteboardMembershipEditingEnabled : AbstractValueObject
+{
+ public bool EditingEnabledValue { get; init; }
+
+ public WhiteboardMembershipEditingEnabled(bool EditingEnabledValue)
+ {
+ this.EditingEnabledValue = EditingEnabledValue;
+ Validate();
+ }
+
+ protected override ICollection GetValidationRules()
+ {
+ return [];
+ }
+}
\ No newline at end of file
diff --git a/dotnet/AipsCore/Domain/Models/WhiteboardMembership/ValueObjects/WhiteboardMembershipId.cs b/dotnet/AipsCore/Domain/Models/WhiteboardMembership/ValueObjects/WhiteboardMembershipId.cs
new file mode 100644
index 0000000..4f786e0
--- /dev/null
+++ b/dotnet/AipsCore/Domain/Models/WhiteboardMembership/ValueObjects/WhiteboardMembershipId.cs
@@ -0,0 +1,8 @@
+using AipsCore.Domain.Common.ValueObjects;
+
+namespace AipsCore.Domain.Models.WhiteboardMembership.ValueObjects;
+
+public record WhiteboardMembershipId(string IdValue) : DomainId(IdValue)
+{
+ public static WhiteboardMembershipId Any() => new(Guid.NewGuid().ToString());
+}
\ No newline at end of file
diff --git a/dotnet/AipsCore/Domain/Models/WhiteboardMembership/ValueObjects/WhiteboardMembershipIsBanned.cs b/dotnet/AipsCore/Domain/Models/WhiteboardMembership/ValueObjects/WhiteboardMembershipIsBanned.cs
new file mode 100644
index 0000000..363e9ab
--- /dev/null
+++ b/dotnet/AipsCore/Domain/Models/WhiteboardMembership/ValueObjects/WhiteboardMembershipIsBanned.cs
@@ -0,0 +1,20 @@
+using AipsCore.Domain.Abstract.Rule;
+using AipsCore.Domain.Abstract.ValueObject;
+
+namespace AipsCore.Domain.Models.WhiteboardMembership.ValueObjects;
+
+public record WhiteboardMembershipIsBanned : AbstractValueObject
+{
+ public bool IsBannedValue { get; init; }
+
+ public WhiteboardMembershipIsBanned(bool IsBannedValue)
+ {
+ this.IsBannedValue = IsBannedValue;
+ Validate();
+ }
+
+ protected override ICollection GetValidationRules()
+ {
+ return [];
+ }
+}
\ No newline at end of file
diff --git a/dotnet/AipsCore/Domain/Models/WhiteboardMembership/ValueObjects/WhiteboardMembershipLastInteractedAt.cs b/dotnet/AipsCore/Domain/Models/WhiteboardMembership/ValueObjects/WhiteboardMembershipLastInteractedAt.cs
new file mode 100644
index 0000000..b9b8952
--- /dev/null
+++ b/dotnet/AipsCore/Domain/Models/WhiteboardMembership/ValueObjects/WhiteboardMembershipLastInteractedAt.cs
@@ -0,0 +1,23 @@
+using AipsCore.Domain.Abstract.Rule;
+using AipsCore.Domain.Abstract.ValueObject;
+using AipsCore.Domain.Common.Validation.Rules;
+
+namespace AipsCore.Domain.Models.WhiteboardMembership.ValueObjects;
+
+public record WhiteboardMembershipLastInteractedAt : AbstractValueObject
+{
+ public DateTime LastInteractedAtValue { get; init; }
+
+ public WhiteboardMembershipLastInteractedAt(DateTime LastInteractedAtValue)
+ {
+ this.LastInteractedAtValue = LastInteractedAtValue;
+ Validate();
+ }
+
+ protected override ICollection GetValidationRules()
+ {
+ return [
+ new DateInPastRule(LastInteractedAtValue)
+ ];
+ }
+}
\ No newline at end of file
diff --git a/dotnet/AipsCore/Domain/Models/WhiteboardMembership/WhiteboardMembership.cs b/dotnet/AipsCore/Domain/Models/WhiteboardMembership/WhiteboardMembership.cs
new file mode 100644
index 0000000..6cd006f
--- /dev/null
+++ b/dotnet/AipsCore/Domain/Models/WhiteboardMembership/WhiteboardMembership.cs
@@ -0,0 +1,105 @@
+using AipsCore.Domain.Models.User.ValueObjects;
+using AipsCore.Domain.Models.Whiteboard.ValueObjects;
+using AipsCore.Domain.Models.WhiteboardMembership.ValueObjects;
+
+namespace AipsCore.Domain.Models.WhiteboardMembership;
+
+public class WhiteboardMembership
+{
+ public WhiteboardMembershipId Id { get; private set; }
+ public WhiteboardId WhiteboardId { get; private set; }
+ public UserId UserId { get; private set; }
+ public WhiteboardMembershipIsBanned IsBanned { get; private set; }
+ public WhiteboardMembershipEditingEnabled EditingEnabled { get; private set; }
+ public WhiteboardMembershipCanJoin CanJoin { get; private set; }
+ public WhiteboardMembershipLastInteractedAt LastInteractedAt { get; private set; }
+
+ public WhiteboardMembership(
+ WhiteboardMembershipId id,
+ Whiteboard.Whiteboard owner,
+ User.User user,
+ WhiteboardMembershipIsBanned isBanned,
+ WhiteboardMembershipEditingEnabled editingEnabled,
+ WhiteboardMembershipCanJoin canJoin,
+ WhiteboardMembershipLastInteractedAt lastInteractedAt)
+ {
+ Id = id;
+ WhiteboardId = owner.Id;
+ UserId = user.Id;
+ IsBanned = isBanned;
+ EditingEnabled = editingEnabled;
+ CanJoin = canJoin;
+ LastInteractedAt = lastInteractedAt;
+ }
+
+ public WhiteboardMembership(
+ WhiteboardMembershipId id,
+ WhiteboardId ownerId,
+ UserId userId,
+ WhiteboardMembershipIsBanned isBanned,
+ WhiteboardMembershipEditingEnabled editingEnabled,
+ WhiteboardMembershipCanJoin canJoin,
+ WhiteboardMembershipLastInteractedAt lastInteractedAt)
+ {
+ Id = id;
+ WhiteboardId = ownerId;
+ UserId = userId;
+ IsBanned = isBanned;
+ EditingEnabled = editingEnabled;
+ CanJoin = canJoin;
+ LastInteractedAt = lastInteractedAt;
+ }
+
+ public static WhiteboardMembership Create(
+ string id,
+ string ownerId,
+ string userId,
+ bool isBanned,
+ bool editingEnabled,
+ bool canJoin,
+ DateTime lastInteractedAt)
+ {
+ var whiteboardMembershipId = new WhiteboardMembershipId(id);
+ var whiteboardId = new WhiteboardId(ownerId);
+ var userIdVo = new UserId(userId);
+ var isBannedVo = new WhiteboardMembershipIsBanned(isBanned);
+ var editingEnabledVo = new WhiteboardMembershipEditingEnabled(editingEnabled);
+ var canJoinVo = new WhiteboardMembershipCanJoin(canJoin);
+ var lastInteractedAtVo = new WhiteboardMembershipLastInteractedAt(lastInteractedAt);
+
+ return new WhiteboardMembership(
+ whiteboardMembershipId,
+ whiteboardId,
+ userIdVo,
+ isBannedVo,
+ editingEnabledVo,
+ canJoinVo,
+ lastInteractedAtVo);
+ }
+
+ public static WhiteboardMembership Create(
+ string ownerId,
+ string userId,
+ bool isBanned,
+ bool editingEnabled,
+ bool canJoin,
+ DateTime lastInteractedAt)
+ {
+ var whiteboardMembershipId = WhiteboardMembershipId.Any();
+ var whiteboardId = new WhiteboardId(ownerId);
+ var userIdVo = new UserId(userId);
+ var isBannedVo = new WhiteboardMembershipIsBanned(isBanned);
+ var editingEnabledVo = new WhiteboardMembershipEditingEnabled(editingEnabled);
+ var canJoinVo = new WhiteboardMembershipCanJoin(canJoin);
+ var lastInteractedAtVo = new WhiteboardMembershipLastInteractedAt(lastInteractedAt);
+
+ return new WhiteboardMembership(
+ whiteboardMembershipId,
+ whiteboardId,
+ userIdVo,
+ isBannedVo,
+ editingEnabledVo,
+ canJoinVo,
+ lastInteractedAtVo);
+ }
+}
\ No newline at end of file
diff --git a/dotnet/AipsCore/Infrastructure/DI/PersistenceRegistrationExtensions.cs b/dotnet/AipsCore/Infrastructure/DI/PersistenceRegistrationExtensions.cs
index 80c2909..182f7e7 100644
--- a/dotnet/AipsCore/Infrastructure/DI/PersistenceRegistrationExtensions.cs
+++ b/dotnet/AipsCore/Infrastructure/DI/PersistenceRegistrationExtensions.cs
@@ -1,6 +1,7 @@
using AipsCore.Domain.Abstract;
using AipsCore.Domain.Models.User.External;
using AipsCore.Domain.Models.Whiteboard.External;
+using AipsCore.Domain.Models.WhiteboardMembership.External;
using AipsCore.Infrastructure.DI.Configuration;
using AipsCore.Infrastructure.Persistence.Db;
using AipsCore.Infrastructure.Persistence.User;
@@ -26,6 +27,7 @@ public static class PersistenceRegistrationExtensions
services.AddTransient();
services.AddTransient();
services.AddTransient();
+ services.AddTransient();
return services;
}
diff --git a/dotnet/AipsCore/Infrastructure/Persistence/Db/AipsDbContext.cs b/dotnet/AipsCore/Infrastructure/Persistence/Db/AipsDbContext.cs
index 8623f8e..cbe5e5d 100644
--- a/dotnet/AipsCore/Infrastructure/Persistence/Db/AipsDbContext.cs
+++ b/dotnet/AipsCore/Infrastructure/Persistence/Db/AipsDbContext.cs
@@ -7,6 +7,7 @@ public class AipsDbContext : DbContext
public DbSet Users { get; set; }
public DbSet Whiteboards { get; set; }
public DbSet Shapes { get; set; }
+ public DbSet WhiteboardMemberships { get; set; }
public AipsDbContext(DbContextOptions options)
: base(options)
diff --git a/dotnet/AipsCore/Infrastructure/Persistence/WhiteboardMembership/WhiteboardMembership.cs b/dotnet/AipsCore/Infrastructure/Persistence/WhiteboardMembership/WhiteboardMembership.cs
new file mode 100644
index 0000000..c2cd8d8
--- /dev/null
+++ b/dotnet/AipsCore/Infrastructure/Persistence/WhiteboardMembership/WhiteboardMembership.cs
@@ -0,0 +1,25 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace AipsCore.Infrastructure.Persistence.WhiteboardMembership;
+
+public class WhiteboardMembership
+{
+ [Key]
+ public Guid Id { get; set; }
+
+ public Guid WhiteboardId { get; set; }
+
+ public Whiteboard.Whiteboard? Whiteboard { get; set; } = null!;
+
+ public Guid UserId { get; set; }
+
+ public User.User? User { get; set; } = null!;
+
+ public bool IsBanned { get; set; }
+
+ public bool EditingEnabled { get; set; }
+
+ public bool CanJoin { get; set; }
+
+ public DateTime LastInteractedAt { get; set; }
+}
\ No newline at end of file
diff --git a/dotnet/AipsCore/Infrastructure/Persistence/WhiteboardMembership/WhiteboardMembershipRepository.cs b/dotnet/AipsCore/Infrastructure/Persistence/WhiteboardMembership/WhiteboardMembershipRepository.cs
new file mode 100644
index 0000000..1913283
--- /dev/null
+++ b/dotnet/AipsCore/Infrastructure/Persistence/WhiteboardMembership/WhiteboardMembershipRepository.cs
@@ -0,0 +1,63 @@
+using AipsCore.Domain.Models.WhiteboardMembership.External;
+using AipsCore.Domain.Models.WhiteboardMembership.ValueObjects;
+using AipsCore.Infrastructure.Persistence.Db;
+
+namespace AipsCore.Infrastructure.Persistence.WhiteboardMembership;
+
+public class WhiteboardMembershipRepository : IWhiteboardMembershipRepository
+{
+ private readonly AipsDbContext _context;
+
+ public WhiteboardMembershipRepository(AipsDbContext context)
+ {
+ _context = context;
+ }
+
+ public async Task Get(WhiteboardMembershipId whiteboardMembershipId, CancellationToken cancellationToken = default)
+ {
+ var whiteboardMembershipEntity = await _context.WhiteboardMemberships.FindAsync(new Guid(whiteboardMembershipId.IdValue));
+
+ if (whiteboardMembershipEntity is null) return null;
+
+ return Domain.Models.WhiteboardMembership.WhiteboardMembership.Create(
+ whiteboardMembershipEntity.Id.ToString(),
+ whiteboardMembershipEntity.WhiteboardId.ToString(),
+ whiteboardMembershipEntity.UserId.ToString(),
+ whiteboardMembershipEntity.IsBanned,
+ whiteboardMembershipEntity.EditingEnabled,
+ whiteboardMembershipEntity.CanJoin,
+ whiteboardMembershipEntity.LastInteractedAt);
+ }
+
+ public async Task Save(Domain.Models.WhiteboardMembership.WhiteboardMembership whiteboardMembership, CancellationToken cancellationToken = default)
+ {
+ var whiteboardMembershipEntity = await _context.WhiteboardMemberships.FindAsync(new Guid(whiteboardMembership.Id.IdValue));
+
+ if (whiteboardMembershipEntity is not null)
+ {
+ whiteboardMembershipEntity.IsBanned = whiteboardMembership.IsBanned.IsBannedValue;
+ whiteboardMembershipEntity.EditingEnabled = whiteboardMembership.EditingEnabled.EditingEnabledValue;
+ whiteboardMembershipEntity.CanJoin = whiteboardMembership.CanJoin.CanJoinValue;
+ whiteboardMembershipEntity.LastInteractedAt = whiteboardMembership.LastInteractedAt.LastInteractedAtValue;
+
+ _context.Update(whiteboardMembershipEntity);
+ }
+ else
+ {
+ whiteboardMembershipEntity = new WhiteboardMembership()
+ {
+ Id = new Guid(whiteboardMembership.Id.IdValue),
+ WhiteboardId = new Guid(whiteboardMembership.WhiteboardId.IdValue),
+ Whiteboard = null,
+ UserId = new Guid(whiteboardMembership.UserId.IdValue),
+ User = null,
+ IsBanned = whiteboardMembership.IsBanned.IsBannedValue,
+ EditingEnabled = whiteboardMembership.EditingEnabled.EditingEnabledValue,
+ CanJoin = whiteboardMembership.CanJoin.CanJoinValue,
+ LastInteractedAt = whiteboardMembership.LastInteractedAt.LastInteractedAtValue
+ };
+
+ _context.Add(whiteboardMembershipEntity);
+ }
+ }
+}
\ No newline at end of file