From 1ab6f643be8c80acad8c3e1d55bd781dfd489e3a Mon Sep 17 00:00:00 2001 From: Hunor Tot-Bagi Date: Mon, 30 Dec 2024 16:01:36 +0200 Subject: [PATCH] Implement Strongly Typed Ids (#33) Co-authored-by: NikolaVetnic --- .../BuildingBlocks.Domain/GlobalUsing.cs | 1 - .../ValueObjects/Ids/FileAssetId.cs | 19 +++------ .../ValueObjects/Ids/NodeId.cs | 35 +++------------- .../ValueObjects/Ids/NoteId.cs | 19 +++------ .../ValueObjects/Ids/ReminderId.cs | 19 +++------ .../ValueObjects/Ids/TimelineId.cs | 19 +++------ .../ValueObjects/StronglyTypedId.cs | 40 +++++++++++++++++++ .../Files.Api/Endpoints/CreateFileAsset.cs | 14 +++---- .../Modules/Files/Files.Api/GlobalUsing.cs | 7 ++++ .../CreateFileAsset/CreateFileAssetCommand.cs | 2 +- .../Files/Files.Domain/Models/FileAsset.cs | 4 +- .../Nodes.Domain/ValueObjects/Ids/PhaseId.cs | 4 +- 12 files changed, 86 insertions(+), 97 deletions(-) create mode 100644 Backend/src/BuildingBlocks/BuildingBlocks.Domain/ValueObjects/StronglyTypedId.cs create mode 100644 Backend/src/Modules/Files/Files.Api/GlobalUsing.cs diff --git a/Backend/src/BuildingBlocks/BuildingBlocks.Domain/GlobalUsing.cs b/Backend/src/BuildingBlocks/BuildingBlocks.Domain/GlobalUsing.cs index 57f9c9c..92f45ae 100644 --- a/Backend/src/BuildingBlocks/BuildingBlocks.Domain/GlobalUsing.cs +++ b/Backend/src/BuildingBlocks/BuildingBlocks.Domain/GlobalUsing.cs @@ -1,2 +1 @@ global using MediatR; -global using BuildingBlocks.Domain.Exceptions; diff --git a/Backend/src/BuildingBlocks/BuildingBlocks.Domain/ValueObjects/Ids/FileAssetId.cs b/Backend/src/BuildingBlocks/BuildingBlocks.Domain/ValueObjects/Ids/FileAssetId.cs index 6589f65..0411b72 100644 --- a/Backend/src/BuildingBlocks/BuildingBlocks.Domain/ValueObjects/Ids/FileAssetId.cs +++ b/Backend/src/BuildingBlocks/BuildingBlocks.Domain/ValueObjects/Ids/FileAssetId.cs @@ -1,20 +1,13 @@ -using BuildingBlocks.Domain.Exceptions; +using System.Text.Json.Serialization; namespace BuildingBlocks.Domain.ValueObjects.Ids; -public record FileAssetId +[JsonConverter(typeof(FileAssetIdJsonConverter))] +public record FileAssetId : StronglyTypedId { - private FileAssetId(Guid value) - { - Value = value; - } + private FileAssetId(Guid value) : base(value) { } - public Guid Value { get; } + public static FileAssetId Of(Guid value) => new(value); - public static FileAssetId Of(Guid value) - { - if (value == Guid.Empty) throw new EmptyIdException("FileAssetId cannot be empty."); - - return new FileAssetId(value); - } + private class FileAssetIdJsonConverter : StronglyTypedIdJsonConverter; } diff --git a/Backend/src/BuildingBlocks/BuildingBlocks.Domain/ValueObjects/Ids/NodeId.cs b/Backend/src/BuildingBlocks/BuildingBlocks.Domain/ValueObjects/Ids/NodeId.cs index e946d5b..d026a7c 100644 --- a/Backend/src/BuildingBlocks/BuildingBlocks.Domain/ValueObjects/Ids/NodeId.cs +++ b/Backend/src/BuildingBlocks/BuildingBlocks.Domain/ValueObjects/Ids/NodeId.cs @@ -1,38 +1,13 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using System.Text.Json.Serialization; namespace BuildingBlocks.Domain.ValueObjects.Ids; [JsonConverter(typeof(NodeIdJsonConverter))] -public record NodeId +public record NodeId : StronglyTypedId { - private NodeId(Guid value) => Value = value; + private NodeId(Guid value) : base(value) { } - public Guid Value { get; } + public static NodeId Of(Guid value) => new(value); - public static NodeId Of(Guid value) - { - if (value == Guid.Empty) - throw new EmptyIdException("NodeId cannot be empty."); - - return new NodeId(value); - } - - public override string ToString() => Value.ToString(); -} - -public class NodeIdJsonConverter : JsonConverter -{ - public override NodeId Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - var value = reader.GetString(); - - if (Guid.TryParse(value, out var guid)) - return NodeId.Of(guid); - - throw new JsonException($"Invalid GUID format for NodeId: {value}"); - } - - public override void Write(Utf8JsonWriter writer, NodeId value, JsonSerializerOptions options) => - writer.WriteStringValue(value.Value.ToString()); + private class NodeIdJsonConverter : StronglyTypedIdJsonConverter; } diff --git a/Backend/src/BuildingBlocks/BuildingBlocks.Domain/ValueObjects/Ids/NoteId.cs b/Backend/src/BuildingBlocks/BuildingBlocks.Domain/ValueObjects/Ids/NoteId.cs index 5ad5564..63436b0 100644 --- a/Backend/src/BuildingBlocks/BuildingBlocks.Domain/ValueObjects/Ids/NoteId.cs +++ b/Backend/src/BuildingBlocks/BuildingBlocks.Domain/ValueObjects/Ids/NoteId.cs @@ -1,20 +1,13 @@ -using BuildingBlocks.Domain.Exceptions; +using System.Text.Json.Serialization; namespace BuildingBlocks.Domain.ValueObjects.Ids; -public record NoteId +[JsonConverter(typeof(NoteIdJsonConverter))] +public record NoteId : StronglyTypedId { - private NoteId(Guid value) - { - Value = value; - } + private NoteId(Guid value) : base(value) { } - public Guid Value { get; } + public static NoteId Of(Guid value) => new(value); - public static NoteId Of(Guid value) - { - if (value == Guid.Empty) throw new EmptyIdException("NoteId cannot be empty."); - - return new NoteId(value); - } + private class NoteIdJsonConverter : StronglyTypedIdJsonConverter; } diff --git a/Backend/src/BuildingBlocks/BuildingBlocks.Domain/ValueObjects/Ids/ReminderId.cs b/Backend/src/BuildingBlocks/BuildingBlocks.Domain/ValueObjects/Ids/ReminderId.cs index 45a1357..9e4195d 100644 --- a/Backend/src/BuildingBlocks/BuildingBlocks.Domain/ValueObjects/Ids/ReminderId.cs +++ b/Backend/src/BuildingBlocks/BuildingBlocks.Domain/ValueObjects/Ids/ReminderId.cs @@ -1,20 +1,13 @@ -using BuildingBlocks.Domain.Exceptions; +using System.Text.Json.Serialization; namespace BuildingBlocks.Domain.ValueObjects.Ids; -public record ReminderId +[JsonConverter(typeof(ReminderIdJsonConverter))] +public record ReminderId : StronglyTypedId { - private ReminderId(Guid value) - { - Value = value; - } + private ReminderId(Guid value) : base(value) { } - public Guid Value { get; } + public static ReminderId Of(Guid value) => new(value); - public static ReminderId Of(Guid value) - { - if (value == Guid.Empty) throw new EmptyIdException("ReminderId cannot be empty."); - - return new ReminderId(value); - } + private class ReminderIdJsonConverter : StronglyTypedIdJsonConverter; } diff --git a/Backend/src/BuildingBlocks/BuildingBlocks.Domain/ValueObjects/Ids/TimelineId.cs b/Backend/src/BuildingBlocks/BuildingBlocks.Domain/ValueObjects/Ids/TimelineId.cs index e044689..fee385e 100644 --- a/Backend/src/BuildingBlocks/BuildingBlocks.Domain/ValueObjects/Ids/TimelineId.cs +++ b/Backend/src/BuildingBlocks/BuildingBlocks.Domain/ValueObjects/Ids/TimelineId.cs @@ -1,20 +1,13 @@ -using BuildingBlocks.Domain.Exceptions; +using System.Text.Json.Serialization; namespace BuildingBlocks.Domain.ValueObjects.Ids; -public record TimelineId +[JsonConverter(typeof(TimelineIdJsonConverter))] +public record TimelineId : StronglyTypedId { - private TimelineId(Guid value) - { - Value = value; - } + private TimelineId(Guid value) : base(value) { } - public Guid Value { get; } + public static TimelineId Of(Guid value) => new(value); - public static TimelineId Of(Guid value) - { - if (value == Guid.Empty) throw new EmptyIdException("TimelineId cannot be empty."); - - return new TimelineId(value); - } + private class TimelineIdJsonConverter : StronglyTypedIdJsonConverter; } diff --git a/Backend/src/BuildingBlocks/BuildingBlocks.Domain/ValueObjects/StronglyTypedId.cs b/Backend/src/BuildingBlocks/BuildingBlocks.Domain/ValueObjects/StronglyTypedId.cs new file mode 100644 index 0000000..41cec1b --- /dev/null +++ b/Backend/src/BuildingBlocks/BuildingBlocks.Domain/ValueObjects/StronglyTypedId.cs @@ -0,0 +1,40 @@ +using System.Text.Json.Serialization; +using System.Text.Json; + +namespace BuildingBlocks.Domain.ValueObjects; + +public abstract record StronglyTypedId +{ + protected StronglyTypedId(Guid value) + { + if (value == Guid.Empty) + throw new ArgumentException($"{GetType().Name} cannot be empty.", nameof(value)); + + Value = value; + } + + public Guid Value { get; } + + public override string ToString() => Value.ToString(); +} + +public class StronglyTypedIdJsonConverter : JsonConverter where T : StronglyTypedId +{ + public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var value = reader.GetString(); + + if (Guid.TryParse(value, out var guid)) + { + var constructor = typeof(T).GetConstructor(new[] { typeof(Guid) }); + + if (constructor != null) + return (T)constructor.Invoke(new object[] { guid }); + } + + throw new JsonException($"Invalid GUID format for {typeof(T).Name}: {value}"); + } + + public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) => + writer.WriteStringValue(value.Value.ToString()); +} diff --git a/Backend/src/Modules/Files/Files.Api/Endpoints/CreateFileAsset.cs b/Backend/src/Modules/Files/Files.Api/Endpoints/CreateFileAsset.cs index 97121bc..b310ac4 100644 --- a/Backend/src/Modules/Files/Files.Api/Endpoints/CreateFileAsset.cs +++ b/Backend/src/Modules/Files/Files.Api/Endpoints/CreateFileAsset.cs @@ -1,12 +1,7 @@ using BuildingBlocks.Domain.ValueObjects.Ids; -using Carter; -using Files.Application.Dtos; using Files.Application.Files.Commands.CreateFileAsset; -using Mapster; -using MediatR; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Routing; + +// ReSharper disable ClassNeverInstantiated.Global namespace Files.Api.Endpoints; @@ -20,7 +15,7 @@ public void AddRoutes(IEndpointRouteBuilder app) var result = await sender.Send(command); var response = result.Adapt(); - return Results.Created($"/Files/{response.AssetId}", response); + return Results.Created($"/Files/{response.Id}", response); }) .WithName("CreateFileAsset") .Produces(StatusCodes.Status201Created) @@ -30,6 +25,7 @@ public void AddRoutes(IEndpointRouteBuilder app) } } +// ReSharper disable once NotAccessedPositionalProperty.Global public record CreateFileAssetRequest(FileAssetDto FileAsset); -public record CreateFileAssetResponse(FileAssetId AssetId); +public record CreateFileAssetResponse(FileAssetId Id); diff --git a/Backend/src/Modules/Files/Files.Api/GlobalUsing.cs b/Backend/src/Modules/Files/Files.Api/GlobalUsing.cs new file mode 100644 index 0000000..b4edfed --- /dev/null +++ b/Backend/src/Modules/Files/Files.Api/GlobalUsing.cs @@ -0,0 +1,7 @@ +global using Carter; +global using Mapster; +global using MediatR; +global using Microsoft.AspNetCore.Builder; +global using Microsoft.AspNetCore.Http; +global using Microsoft.AspNetCore.Routing; +global using Files.Application.Dtos; diff --git a/Backend/src/Modules/Files/Files.Application/Files/Commands/CreateFileAsset/CreateFileAssetCommand.cs b/Backend/src/Modules/Files/Files.Application/Files/Commands/CreateFileAsset/CreateFileAssetCommand.cs index fda3141..9484d3e 100644 --- a/Backend/src/Modules/Files/Files.Application/Files/Commands/CreateFileAsset/CreateFileAssetCommand.cs +++ b/Backend/src/Modules/Files/Files.Application/Files/Commands/CreateFileAsset/CreateFileAssetCommand.cs @@ -6,7 +6,7 @@ namespace Files.Application.Files.Commands.CreateFileAsset; public record CreateFileAssetCommand(FileAssetDto FileAsset) : ICommand; -public record CreateFileAssetResult(FileAssetId AssetId); +public record CreateFileAssetResult(FileAssetId Id); public class CreateFileCommandValidator : AbstractValidator { diff --git a/Backend/src/Modules/Files/Files.Domain/Models/FileAsset.cs b/Backend/src/Modules/Files/Files.Domain/Models/FileAsset.cs index 0de98d4..eea9043 100644 --- a/Backend/src/Modules/Files/Files.Domain/Models/FileAsset.cs +++ b/Backend/src/Modules/Files/Files.Domain/Models/FileAsset.cs @@ -16,11 +16,11 @@ public class FileAsset : Aggregate #region File - public static FileAsset Create(FileAssetId fileAssetId, string name, float size, string type, string owner, string description, List sharedWith) + public static FileAsset Create(FileAssetId id, string name, float size, string type, string owner, string description, List sharedWith) { var file = new FileAsset { - Id = fileAssetId, + Id = id, Name = name, Size = size, Type = type, diff --git a/Backend/src/Modules/Nodes/Nodes.Domain/ValueObjects/Ids/PhaseId.cs b/Backend/src/Modules/Nodes/Nodes.Domain/ValueObjects/Ids/PhaseId.cs index 05a75b3..dd50225 100644 --- a/Backend/src/Modules/Nodes/Nodes.Domain/ValueObjects/Ids/PhaseId.cs +++ b/Backend/src/Modules/Nodes/Nodes.Domain/ValueObjects/Ids/PhaseId.cs @@ -13,8 +13,8 @@ private PhaseId(Guid value) public static PhaseId Of(Guid value) { - ArgumentNullException.ThrowIfNull(value); - if (value == Guid.Empty) throw new EmptyIdException("PhaseId cannot be empty."); + if (value == Guid.Empty) + throw new EmptyIdException("PhaseId cannot be empty."); return new PhaseId(value); }