diff --git a/dotnet/AipsCore/Application/Models/Shape/Command/CreateRectangle/CreateRectangleCommand.cs b/dotnet/AipsCore/Application/Models/Shape/Command/CreateRectangle/CreateRectangleCommand.cs new file mode 100644 index 0000000..f1cbb39 --- /dev/null +++ b/dotnet/AipsCore/Application/Models/Shape/Command/CreateRectangle/CreateRectangleCommand.cs @@ -0,0 +1,13 @@ +using AipsCore.Application.Abstract.Command; +using AipsCore.Domain.Models.Shape.ValueObjects; + +namespace AipsCore.Application.Models.Shape.Command.CreateRectangle; + +public record CreateRectangleCommand( + string WhiteboardId, + int PositionX, + int PositionY, + string Color, + int EndPositionX, + int EndPositionY, + int BorderThickness) : ICommand; \ No newline at end of file diff --git a/dotnet/AipsCore/Application/Models/Shape/Command/CreateRectangle/CreateRectangleCommandHandler.cs b/dotnet/AipsCore/Application/Models/Shape/Command/CreateRectangle/CreateRectangleCommandHandler.cs new file mode 100644 index 0000000..35b8733 --- /dev/null +++ b/dotnet/AipsCore/Application/Models/Shape/Command/CreateRectangle/CreateRectangleCommandHandler.cs @@ -0,0 +1,35 @@ +using AipsCore.Application.Abstract.Command; +using AipsCore.Domain.Abstract; +using AipsCore.Domain.Models.Shape.External; +using AipsCore.Domain.Models.Shape.Sub.Rectangle; +using AipsCore.Domain.Models.Shape.ValueObjects; + +namespace AipsCore.Application.Models.Shape.Command.CreateRectangle; + +public class CreateRectangleCommandHandler : ICommandHandler +{ + private readonly IShapeRepository _shapeRepository; + private readonly IUnitOfWork _unitOfWork; + + public CreateRectangleCommandHandler(IShapeRepository shapeRepository, IUnitOfWork unitOfWork) + { + _shapeRepository = shapeRepository; + _unitOfWork = unitOfWork; + } + + public async Task Handle(CreateRectangleCommand command, CancellationToken cancellationToken = default) + { + var rectangle = Rectangle.Create( + command.WhiteboardId, + command.PositionX, command.PositionY, + command.Color, + command.EndPositionX, + command.EndPositionY, + command.BorderThickness); + + await _shapeRepository.Add(rectangle, cancellationToken); + await _unitOfWork.SaveChangesAsync(cancellationToken); + + return rectangle.Id; + } +} \ No newline at end of file diff --git a/dotnet/AipsCore/Domain/Models/Shape/External/IShapeRepository.cs b/dotnet/AipsCore/Domain/Models/Shape/External/IShapeRepository.cs new file mode 100644 index 0000000..f9c0d40 --- /dev/null +++ b/dotnet/AipsCore/Domain/Models/Shape/External/IShapeRepository.cs @@ -0,0 +1,9 @@ +using AipsCore.Domain.Models.Shape.ValueObjects; + +namespace AipsCore.Domain.Models.Shape.External; + +public interface IShapeRepository +{ + Task Get(ShapeId id, CancellationToken cancellationToken = default); + Task Add(Shape shape, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/dotnet/AipsCore/Domain/Models/Shape/Shape.cs b/dotnet/AipsCore/Domain/Models/Shape/Shape.cs index 1c1dcbe..9a96750 100644 --- a/dotnet/AipsCore/Domain/Models/Shape/Shape.cs +++ b/dotnet/AipsCore/Domain/Models/Shape/Shape.cs @@ -7,7 +7,7 @@ namespace AipsCore.Domain.Models.Shape; public abstract class Shape { - public ShapeId Id { get; } + public ShapeId Id { get; init; } public WhiteboardId WhiteboardId { get; private set; } @@ -24,4 +24,16 @@ public abstract class Shape Color = color; WhiteboardId = whiteboardId; } + + protected Shape( + string id, + string whiteboardId, + int positionX, int positionY, + string color) + { + Id = new ShapeId(id); + Position = new Position(positionX, positionY); + Color = new Color(color); + WhiteboardId = new WhiteboardId(whiteboardId); + } } \ No newline at end of file diff --git a/dotnet/AipsCore/Domain/Models/Shape/Sub/Arrow/Arrow.cs b/dotnet/AipsCore/Domain/Models/Shape/Sub/Arrow/Arrow.cs index cd956cf..5a3a956 100644 --- a/dotnet/AipsCore/Domain/Models/Shape/Sub/Arrow/Arrow.cs +++ b/dotnet/AipsCore/Domain/Models/Shape/Sub/Arrow/Arrow.cs @@ -17,4 +17,21 @@ public class Arrow : Shape } public override ShapeType ShapeType => ShapeType.Arrow; + + public static Arrow Create( + string id, + string whiteboardId, + int positionX, int positionY, + string color, + int endPositionX, int endPositionY, + int borderThickness) + { + return new Arrow( + new ShapeId(id), + new WhiteboardId(whiteboardId), + new Position(positionX, positionY), + new Color(color), + new Position(endPositionX, endPositionY), + new Thickness(borderThickness)); + } } \ No newline at end of file diff --git a/dotnet/AipsCore/Domain/Models/Shape/Sub/Line/Line.cs b/dotnet/AipsCore/Domain/Models/Shape/Sub/Line/Line.cs index fc81c44..a17d87f 100644 --- a/dotnet/AipsCore/Domain/Models/Shape/Sub/Line/Line.cs +++ b/dotnet/AipsCore/Domain/Models/Shape/Sub/Line/Line.cs @@ -17,4 +17,21 @@ public class Line : Shape } public override ShapeType ShapeType => ShapeType.Line; + + public static Line Create( + string id, + string whiteboardId, + int positionX, int positionY, + string color, + int endPositionX, int endPositionY, + int borderThickness) + { + return new Line( + new ShapeId(id), + new WhiteboardId(whiteboardId), + new Position(positionX, positionY), + new Color(color), + new Position(endPositionX, endPositionY), + new Thickness(borderThickness)); + } } \ No newline at end of file diff --git a/dotnet/AipsCore/Domain/Models/Shape/Sub/Rectangle/Rectangle.cs b/dotnet/AipsCore/Domain/Models/Shape/Sub/Rectangle/Rectangle.cs index 188125e..cf1e099 100644 --- a/dotnet/AipsCore/Domain/Models/Shape/Sub/Rectangle/Rectangle.cs +++ b/dotnet/AipsCore/Domain/Models/Shape/Sub/Rectangle/Rectangle.cs @@ -20,4 +20,36 @@ public class Rectangle : Shape BorderThickness = borderThickness; } + public static Rectangle Create( + string id, + string whiteboardId, + int positionX, int positionY, + string color, + int endPositionX, int endPositionY, + int borderThickness) + { + return new Rectangle( + new ShapeId(id), + new WhiteboardId(whiteboardId), + new Position(positionX, positionY), + new Color(color), + new Position(endPositionX, endPositionY), + new Thickness(borderThickness)); + } + + public static Rectangle Create( + string whiteboardId, + int positionX, int positionY, + string color, + int endPositionX, int endPositionY, + int borderThickness) + { + return new Rectangle( + ShapeId.Any(), + new WhiteboardId(whiteboardId), + new Position(positionX, positionY), + new Color(color), + new Position(endPositionX, endPositionY), + new Thickness(borderThickness)); + } } \ No newline at end of file diff --git a/dotnet/AipsCore/Domain/Models/Shape/Sub/TextShape/TextShape.cs b/dotnet/AipsCore/Domain/Models/Shape/Sub/TextShape/TextShape.cs index e6efb2a..2568c43 100644 --- a/dotnet/AipsCore/Domain/Models/Shape/Sub/TextShape/TextShape.cs +++ b/dotnet/AipsCore/Domain/Models/Shape/Sub/TextShape/TextShape.cs @@ -20,4 +20,20 @@ public class TextShape : Shape } public override ShapeType ShapeType => ShapeType.Text; + + public static TextShape Create( + string id, + string whiteboardId, + int positionX, int positionY, + string color, + string textValue, int textSize) + { + return new TextShape( + new ShapeId(id), + new WhiteboardId(whiteboardId), + new Position(positionX, positionY), + new Color(color), + new TextShapeValue(textValue), + new TextShapeSize(textSize)); + } } \ No newline at end of file diff --git a/dotnet/AipsCore/Domain/Models/Shape/ValueObjects/ShapeId.cs b/dotnet/AipsCore/Domain/Models/Shape/ValueObjects/ShapeId.cs index bfc6cc4..7190e7c 100644 --- a/dotnet/AipsCore/Domain/Models/Shape/ValueObjects/ShapeId.cs +++ b/dotnet/AipsCore/Domain/Models/Shape/ValueObjects/ShapeId.cs @@ -2,4 +2,7 @@ using AipsCore.Domain.Common.ValueObjects; namespace AipsCore.Domain.Models.Shape.ValueObjects; -public record ShapeId(string Value) : DomainId(Value); \ No newline at end of file +public record ShapeId(string Value) : DomainId(Value) +{ + public static ShapeId Any() => new ShapeId(Guid.NewGuid().ToString()); +} \ No newline at end of file diff --git a/dotnet/AipsCore/Domain/Models/Shape/ValueObjects/Thickness.cs b/dotnet/AipsCore/Domain/Models/Shape/ValueObjects/Thickness.cs index ce38c61..19d7bf8 100644 --- a/dotnet/AipsCore/Domain/Models/Shape/ValueObjects/Thickness.cs +++ b/dotnet/AipsCore/Domain/Models/Shape/ValueObjects/Thickness.cs @@ -6,22 +6,23 @@ namespace AipsCore.Domain.Models.Shape.ValueObjects; public record Thickness : AbstractValueObject { + public int Value { get; } private const int MaxThickness = 8; private const int MinThickness = 1; - private readonly int _value; public Thickness(int value) { - _value = value; + Value = value; + Validate(); } protected override ICollection GetValidationRules() { return [ - new MinValueRule(_value, MinThickness), - new MaxValueRule(_value, MaxThickness), + new MinValueRule(Value, MinThickness), + new MaxValueRule(Value, MaxThickness), ]; } } \ No newline at end of file diff --git a/dotnet/AipsCore/Infrastructure/DI/PersistenceRegistrationExtensions.cs b/dotnet/AipsCore/Infrastructure/DI/PersistenceRegistrationExtensions.cs index 182f7e7..1f0f8bd 100644 --- a/dotnet/AipsCore/Infrastructure/DI/PersistenceRegistrationExtensions.cs +++ b/dotnet/AipsCore/Infrastructure/DI/PersistenceRegistrationExtensions.cs @@ -1,9 +1,11 @@ using AipsCore.Domain.Abstract; +using AipsCore.Domain.Models.Shape.External; 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.Shape; using AipsCore.Infrastructure.Persistence.User; using AipsCore.Infrastructure.Persistence.Whiteboard; using Microsoft.EntityFrameworkCore; @@ -28,6 +30,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 35e96ef..cbe5e5d 100644 --- a/dotnet/AipsCore/Infrastructure/Persistence/Db/AipsDbContext.cs +++ b/dotnet/AipsCore/Infrastructure/Persistence/Db/AipsDbContext.cs @@ -6,6 +6,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) diff --git a/dotnet/AipsCore/Infrastructure/Persistence/Shape/Mappers/ShapeMappers.FromEntity.cs b/dotnet/AipsCore/Infrastructure/Persistence/Shape/Mappers/ShapeMappers.FromEntity.cs new file mode 100644 index 0000000..f1c51ad --- /dev/null +++ b/dotnet/AipsCore/Infrastructure/Persistence/Shape/Mappers/ShapeMappers.FromEntity.cs @@ -0,0 +1,65 @@ +using AipsCore.Domain.Models.Shape.Enums; +using AipsCore.Domain.Models.Shape.Sub.Arrow; +using AipsCore.Domain.Models.Shape.Sub.Line; +using AipsCore.Domain.Models.Shape.Sub.Rectangle; +using AipsCore.Domain.Models.Shape.Sub.TextShape; + +namespace AipsCore.Infrastructure.Persistence.Shape.Mappers; + +public static partial class ShapeMappers +{ + public static Domain.Models.Shape.Shape EntityToModel(Shape shape) + { + return shape.Type switch + { + ShapeType.Rectangle => EntityToRectangle(shape), + ShapeType.Line => EntityToLine(shape), + ShapeType.Arrow => EntityToArrow(shape), + ShapeType.Text => EntityToTextShape(shape), + _ => throw new ArgumentOutOfRangeException() + }; + } + + public static Rectangle EntityToRectangle(Shape shape) + { + return Rectangle.Create( + shape.Id.ToString(), + shape.WhiteboardId.ToString(), + shape.PositionX, shape.PositionY, + shape.Color, + shape.EndPositionX!.Value, shape.EndPositionY!.Value, + shape.Thickness!.Value); + } + + public static Line EntityToLine(Shape shape) + { + return Line.Create( + shape.Id.ToString(), + shape.WhiteboardId.ToString(), + shape.PositionX, shape.PositionY, + shape.Color, + shape.EndPositionX!.Value, shape.EndPositionY!.Value, + shape.Thickness!.Value); + } + + public static Arrow EntityToArrow(Shape shape) + { + return Arrow.Create( + shape.Id.ToString(), + shape.WhiteboardId.ToString(), + shape.PositionX, shape.PositionY, + shape.Color, + shape.EndPositionX!.Value, shape.EndPositionY!.Value, + shape.Thickness!.Value); + } + + public static TextShape EntityToTextShape(Shape shape) + { + return TextShape.Create( + shape.Id.ToString(), + shape.WhiteboardId.ToString(), + shape.PositionX, shape.PositionY, + shape.Color, + shape.TextValue!, shape.TextSize!.Value); + } +} \ No newline at end of file diff --git a/dotnet/AipsCore/Infrastructure/Persistence/Shape/Mappers/ShapeMappers.ToEntity.cs b/dotnet/AipsCore/Infrastructure/Persistence/Shape/Mappers/ShapeMappers.ToEntity.cs new file mode 100644 index 0000000..89edea7 --- /dev/null +++ b/dotnet/AipsCore/Infrastructure/Persistence/Shape/Mappers/ShapeMappers.ToEntity.cs @@ -0,0 +1,90 @@ +using AipsCore.Domain.Models.Shape.Enums; +using AipsCore.Domain.Models.Shape.Sub.Arrow; +using AipsCore.Domain.Models.Shape.Sub.Line; +using AipsCore.Domain.Models.Shape.Sub.Rectangle; +using AipsCore.Domain.Models.Shape.Sub.TextShape; + +namespace AipsCore.Infrastructure.Persistence.Shape.Mappers; + +public static partial class ShapeMappers +{ + + public static Shape ModelToEntity(Domain.Models.Shape.Shape model) + { + return model.ShapeType switch + { + ShapeType.Rectangle => RectangleToEntity((Rectangle)model), + ShapeType.Line => LineToEntity((Line)model), + ShapeType.Arrow => ArrowToEntity((Arrow)model), + ShapeType.Text => TextShapeToEntity((TextShape)model), + _ => throw new ArgumentOutOfRangeException() + }; + } + + public static Shape RectangleToEntity(Rectangle rectangle) + { + return new Shape() + { + Id = new Guid(rectangle.Id.Value), + Type = rectangle.ShapeType, + WhiteboardId = new Guid(rectangle.WhiteboardId.IdValue), + PositionX = rectangle.Position.X, + PositionY = rectangle.Position.Y, + Color = rectangle.Color.Value, + //SPECIFIC + EndPositionX = rectangle.EndPosition.X, + EndPositionY = rectangle.EndPosition.Y, + Thickness = rectangle.BorderThickness.Value, + }; + } + + public static Shape LineToEntity(Line line) + { + return new Shape() + { + Id = new Guid(line.Id.Value), + Type = line.ShapeType, + WhiteboardId = new Guid(line.WhiteboardId.IdValue), + PositionX = line.Position.X, + PositionY = line.Position.Y, + Color = line.Color.Value, + //SPECIFIC + EndPositionX = line.EndPosition.X, + EndPositionY = line.EndPosition.Y, + Thickness = line.Thickness.Value, + }; + } + + public static Shape ArrowToEntity(Arrow arrow) + { + return new Shape() + { + Id = new Guid(arrow.Id.Value), + Type = arrow.ShapeType, + WhiteboardId = new Guid(arrow.WhiteboardId.IdValue), + PositionX = arrow.Position.X, + PositionY = arrow.Position.Y, + Color = arrow.Color.Value, + //SPECIFIC + EndPositionX = arrow.EndPosition.X, + EndPositionY = arrow.EndPosition.Y, + Thickness = arrow.Thickness.Value, + }; + } + + public static Shape TextShapeToEntity(TextShape textShape) + { + return new Shape() + { + Id = new Guid(textShape.Id.Value), + Type = textShape.ShapeType, + WhiteboardId = new Guid(textShape.WhiteboardId.IdValue), + PositionX = textShape.Position.X, + PositionY = textShape.Position.Y, + Color = textShape.Color.Value, + //SPECIFIC + TextValue = textShape.TextShapeValue.Text, + TextSize = textShape.TextShapeSize.Size, + }; + } +} \ No newline at end of file diff --git a/dotnet/AipsCore/Infrastructure/Persistence/Shape/Mappers/ShapeMappers.UpdateEntity.cs b/dotnet/AipsCore/Infrastructure/Persistence/Shape/Mappers/ShapeMappers.UpdateEntity.cs new file mode 100644 index 0000000..12083e9 --- /dev/null +++ b/dotnet/AipsCore/Infrastructure/Persistence/Shape/Mappers/ShapeMappers.UpdateEntity.cs @@ -0,0 +1,61 @@ +using AipsCore.Domain.Models.Shape.Enums; +using AipsCore.Domain.Models.Shape.Sub.Arrow; +using AipsCore.Domain.Models.Shape.Sub.Line; +using AipsCore.Domain.Models.Shape.Sub.Rectangle; +using AipsCore.Domain.Models.Shape.Sub.TextShape; + +namespace AipsCore.Infrastructure.Persistence.Shape.Mappers; + +public static partial class ShapeMappers +{ + public static void UpdateEntity(Shape entity, Domain.Models.Shape.Shape model) + { + entity.WhiteboardId = new Guid(model.WhiteboardId.IdValue); + entity.PositionX = model.Position.X; + entity.PositionY = model.Position.Y; + entity.Color = model.Color.Value; + + switch (model.ShapeType) + { + case ShapeType.Rectangle: + UpdateEntityFromRectangle(entity, (Rectangle)model); + break; + case ShapeType.Line: + UpdateEntityFromLine(entity, (Line)model); + break; + case ShapeType.Arrow: + UpdateEntityFromArrow(entity, (Arrow)model); + break; + case ShapeType.Text: + UpdateEntityFromTextShape(entity, (TextShape)model); + break; + }; + } + + public static void UpdateEntityFromRectangle(Shape entity, Rectangle rectangle) + { + entity.EndPositionX = rectangle.EndPosition.X; + entity.EndPositionY = rectangle.EndPosition.Y; + entity.Thickness = rectangle.BorderThickness.Value; + } + + public static void UpdateEntityFromLine(Shape entity, Line line) + { + entity.EndPositionX = line.EndPosition.X; + entity.EndPositionY = line.EndPosition.Y; + entity.Thickness = line.Thickness.Value; + } + + public static void UpdateEntityFromArrow(Shape entity, Arrow arrow) + { + entity.EndPositionX = arrow.EndPosition.X; + entity.EndPositionY = arrow.EndPosition.Y; + entity.Thickness = arrow.Thickness.Value; + } + + public static void UpdateEntityFromTextShape(Shape entity, TextShape textShape) + { + entity.TextValue = textShape.TextShapeValue.Text; + entity.TextSize = textShape.TextShapeSize.Size; + } +} \ No newline at end of file diff --git a/dotnet/AipsCore/Infrastructure/Persistence/Shape/Shape.cs b/dotnet/AipsCore/Infrastructure/Persistence/Shape/Shape.cs new file mode 100644 index 0000000..7d07e1b --- /dev/null +++ b/dotnet/AipsCore/Infrastructure/Persistence/Shape/Shape.cs @@ -0,0 +1,33 @@ +using System.ComponentModel.DataAnnotations; +using AipsCore.Domain.Models.Shape.Enums; + +namespace AipsCore.Infrastructure.Persistence.Shape; + + +public class Shape +{ + [Key] + public Guid Id { get; set; } + + // NAV TO WHITEBOARD + public Guid WhiteboardId { get; set; } + public Whiteboard.Whiteboard Whiteboard { get; set; } = null!; + + public ShapeType Type { get; set; } + + public int PositionX { get; set; } + public int PositionY { get; set; } + + [MaxLength(10)] public string Color { get; set; } = null!; + + // END POSITION (Rectangle, Line, Arrow) + public int? EndPositionX { get; set; } + public int? EndPositionY { get; set; } + + // THICKNESS (Rectangle, Line, Arrow) + public int? Thickness { get; set; } + + // TEXT (Text) + public string? TextValue { get; set; } + public int? TextSize { get; set; } +} \ No newline at end of file diff --git a/dotnet/AipsCore/Infrastructure/Persistence/Shape/ShapeRepository.cs b/dotnet/AipsCore/Infrastructure/Persistence/Shape/ShapeRepository.cs new file mode 100644 index 0000000..62cb106 --- /dev/null +++ b/dotnet/AipsCore/Infrastructure/Persistence/Shape/ShapeRepository.cs @@ -0,0 +1,42 @@ +using AipsCore.Domain.Models.Shape.External; +using AipsCore.Domain.Models.Shape.ValueObjects; +using AipsCore.Infrastructure.Persistence.Db; +using AipsCore.Infrastructure.Persistence.Shape.Mappers; + +namespace AipsCore.Infrastructure.Persistence.Shape; + +public class ShapeRepository : IShapeRepository +{ + private readonly AipsDbContext _context; + + public ShapeRepository(AipsDbContext context) + { + _context = context; + } + + public async Task Get(ShapeId id, CancellationToken cancellationToken = default) + { + var entity = await _context.Shapes.FindAsync([new Guid(id.Value)], cancellationToken); + + if (entity is null) return null; + + return ShapeMappers.EntityToModel(entity); + } + + public async Task Add(Domain.Models.Shape.Shape shape, CancellationToken cancellationToken = default) + { + var entity = await _context.Shapes.FindAsync([new Guid(shape.Id.Value)], cancellationToken); + + if (entity is not null) + { + ShapeMappers.UpdateEntity(entity, shape); + } + else + { + var newEntity = ShapeMappers.ModelToEntity(shape); + + await _context.Shapes.AddAsync(newEntity, cancellationToken); + } + + } +} \ No newline at end of file