From e33c270fde486b2964f1b13993760ab949bec51c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1t=C3=A9=20Farkas?= Date: Thu, 26 Dec 2024 14:23:50 +0100 Subject: [PATCH] refactor: Refactor Get connections --- ...31510_RenameCategoriesPuzzles2.Designer.cs | 135 ++++++++++++++++++ ...20241226131510_RenameCategoriesPuzzles2.cs | 79 ++++++++++ .../ConnectionsContextModelSnapshot.cs | 11 +- Features/Connections/ConnectionsGroup.cs | 14 ++ Features/Connections/Get/Endpoint.cs | 35 +++++ Features/Puzzle/Get/GetPuzzleEndpoint.cs | 112 +++++++-------- Features/Puzzle/List/ListPuzzlesEndpoint.cs | 56 ++++---- Models/PuzzleDTO.cs | 55 ------- Models/Request/GetPuzzleRequest.cs | 3 + Models/Response/ConnectionsPuzzleDTO.cs | 53 +++++++ Validators/GetPuzzleRequestValidator.cs | 23 +++ 11 files changed, 431 insertions(+), 145 deletions(-) create mode 100644 Database/Migrations/20241226131510_RenameCategoriesPuzzles2.Designer.cs create mode 100644 Database/Migrations/20241226131510_RenameCategoriesPuzzles2.cs create mode 100644 Features/Connections/ConnectionsGroup.cs create mode 100644 Features/Connections/Get/Endpoint.cs delete mode 100644 Models/PuzzleDTO.cs create mode 100644 Models/Request/GetPuzzleRequest.cs create mode 100644 Models/Response/ConnectionsPuzzleDTO.cs create mode 100644 Validators/GetPuzzleRequestValidator.cs diff --git a/Database/Migrations/20241226131510_RenameCategoriesPuzzles2.Designer.cs b/Database/Migrations/20241226131510_RenameCategoriesPuzzles2.Designer.cs new file mode 100644 index 0000000..38a8acc --- /dev/null +++ b/Database/Migrations/20241226131510_RenameCategoriesPuzzles2.Designer.cs @@ -0,0 +1,135 @@ +// +using System; +using ConnectionsAPI.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace ConnectionsAPI.Database.Migrations +{ + [DbContext(typeof(ConnectionsContext))] + [Migration("20241226131510_RenameCategoriesPuzzles2")] + partial class RenameCategoriesPuzzles2 + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.4"); + + modelBuilder.Entity("ConnectionsAPI.Database.Entities.ConnectionsCard", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ConnectionsCategoryId") + .HasColumnType("INTEGER"); + + b.Property("Content") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Position") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ConnectionsCategoryId"); + + b.ToTable("ConnectionsCard"); + }); + + modelBuilder.Entity("ConnectionsAPI.Database.Entities.ConnectionsCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Color") + .HasColumnType("INTEGER"); + + b.Property("ConnectionsPuzzleId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ConnectionsPuzzleId"); + + b.ToTable("ConnectionsCategory"); + }); + + modelBuilder.Entity("ConnectionsAPI.Database.Entities.ConnectionsPuzzle", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ContentMD5") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedDate") + .HasColumnType("TEXT"); + + b.Property("EditorName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Index") + .HasColumnType("INTEGER"); + + b.Property("PrintDate") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("PrintDate") + .IsUnique(); + + b.ToTable("ConnectionsPuzzles"); + }); + + modelBuilder.Entity("ConnectionsAPI.Database.Entities.ConnectionsCard", b => + { + b.HasOne("ConnectionsAPI.Database.Entities.ConnectionsCategory", "Category") + .WithMany("Cards") + .HasForeignKey("ConnectionsCategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("ConnectionsAPI.Database.Entities.ConnectionsCategory", b => + { + b.HasOne("ConnectionsAPI.Database.Entities.ConnectionsPuzzle", "ConnectionsPuzzle") + .WithMany("Categories") + .HasForeignKey("ConnectionsPuzzleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ConnectionsPuzzle"); + }); + + modelBuilder.Entity("ConnectionsAPI.Database.Entities.ConnectionsCategory", b => + { + b.Navigation("Cards"); + }); + + modelBuilder.Entity("ConnectionsAPI.Database.Entities.ConnectionsPuzzle", b => + { + b.Navigation("Categories"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Database/Migrations/20241226131510_RenameCategoriesPuzzles2.cs b/Database/Migrations/20241226131510_RenameCategoriesPuzzles2.cs new file mode 100644 index 0000000..2dee41e --- /dev/null +++ b/Database/Migrations/20241226131510_RenameCategoriesPuzzles2.cs @@ -0,0 +1,79 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace ConnectionsAPI.Database.Migrations +{ + /// + public partial class RenameCategoriesPuzzles2 : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_ConnectionsCard_ConnectionsCategory_CategoryId", + table: "ConnectionsCard"); + + migrationBuilder.DropIndex( + name: "IX_ConnectionsCard_CategoryId", + table: "ConnectionsCard"); + + migrationBuilder.DropColumn( + name: "CategoryId", + table: "ConnectionsCard"); + + migrationBuilder.RenameColumn( + name: "CategoriesCategoryId", + table: "ConnectionsCard", + newName: "ConnectionsCategoryId"); + + migrationBuilder.CreateIndex( + name: "IX_ConnectionsCard_ConnectionsCategoryId", + table: "ConnectionsCard", + column: "ConnectionsCategoryId"); + + migrationBuilder.AddForeignKey( + name: "FK_ConnectionsCard_ConnectionsCategory_ConnectionsCategoryId", + table: "ConnectionsCard", + column: "ConnectionsCategoryId", + principalTable: "ConnectionsCategory", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_ConnectionsCard_ConnectionsCategory_ConnectionsCategoryId", + table: "ConnectionsCard"); + + migrationBuilder.DropIndex( + name: "IX_ConnectionsCard_ConnectionsCategoryId", + table: "ConnectionsCard"); + + migrationBuilder.RenameColumn( + name: "ConnectionsCategoryId", + table: "ConnectionsCard", + newName: "CategoriesCategoryId"); + + migrationBuilder.AddColumn( + name: "CategoryId", + table: "ConnectionsCard", + type: "INTEGER", + nullable: true); + + migrationBuilder.CreateIndex( + name: "IX_ConnectionsCard_CategoryId", + table: "ConnectionsCard", + column: "CategoryId"); + + migrationBuilder.AddForeignKey( + name: "FK_ConnectionsCard_ConnectionsCategory_CategoryId", + table: "ConnectionsCard", + column: "CategoryId", + principalTable: "ConnectionsCategory", + principalColumn: "Id"); + } + } +} diff --git a/Database/Migrations/ConnectionsContextModelSnapshot.cs b/Database/Migrations/ConnectionsContextModelSnapshot.cs index 35a38fb..ab84cf7 100644 --- a/Database/Migrations/ConnectionsContextModelSnapshot.cs +++ b/Database/Migrations/ConnectionsContextModelSnapshot.cs @@ -23,10 +23,7 @@ namespace ConnectionsAPI.Database.Migrations .ValueGeneratedOnAdd() .HasColumnType("INTEGER"); - b.Property("CategoriesCategoryId") - .HasColumnType("INTEGER"); - - b.Property("CategoryId") + b.Property("ConnectionsCategoryId") .HasColumnType("INTEGER"); b.Property("Content") @@ -38,7 +35,7 @@ namespace ConnectionsAPI.Database.Migrations b.HasKey("Id"); - b.HasIndex("CategoryId"); + b.HasIndex("ConnectionsCategoryId"); b.ToTable("ConnectionsCard"); }); @@ -102,7 +99,9 @@ namespace ConnectionsAPI.Database.Migrations { b.HasOne("ConnectionsAPI.Database.Entities.ConnectionsCategory", "Category") .WithMany("Cards") - .HasForeignKey("CategoryId"); + .HasForeignKey("ConnectionsCategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); b.Navigation("Category"); }); diff --git a/Features/Connections/ConnectionsGroup.cs b/Features/Connections/ConnectionsGroup.cs new file mode 100644 index 0000000..957d9e1 --- /dev/null +++ b/Features/Connections/ConnectionsGroup.cs @@ -0,0 +1,14 @@ +using System; + +namespace ConnectionsAPI.Features.Connections; + +public class ConnectionsGroup : Group +{ + public ConnectionsGroup() + { + Configure("connections", ep => + { + ep.AllowAnonymous(); + }); + } +} \ No newline at end of file diff --git a/Features/Connections/Get/Endpoint.cs b/Features/Connections/Get/Endpoint.cs new file mode 100644 index 0000000..2727e52 --- /dev/null +++ b/Features/Connections/Get/Endpoint.cs @@ -0,0 +1,35 @@ +using ConnectionsAPI.Database.Repository; +using ConnectionsAPI.Models.Request; +using ConnectionsAPI.Models.Response; + +namespace ConnectionsAPI.Features.Connections.Get; + +public class GetConnectionsEndpoint(PuzzleRepository _puzzleRepo) : Endpoint +{ + public override void Configure() + { + Get("{PrintDate}"); + Group(); + } + + public override async Task HandleAsync(GetPuzzleRequest req, CancellationToken ct) + { + bool hideSolutions = Query("hideSolutions", isRequired: false); + + // query for the puzzle + var puzzle = await _puzzleRepo.GetPuzzleByDateAsync(req.PrintDate, includeSolutions: !hideSolutions); + + // if not found, done here + if (puzzle == null) + { + await SendNotFoundAsync(ct); + return; + } + + // get response from cache + var response = ConnectionsPuzzleDTO.FromEntity(puzzle); + + // done + await SendAsync(response, cancellation: ct); + } +} \ No newline at end of file diff --git a/Features/Puzzle/Get/GetPuzzleEndpoint.cs b/Features/Puzzle/Get/GetPuzzleEndpoint.cs index 834defd..96c06fa 100644 --- a/Features/Puzzle/Get/GetPuzzleEndpoint.cs +++ b/Features/Puzzle/Get/GetPuzzleEndpoint.cs @@ -1,67 +1,67 @@ -using ConnectionsAPI.Database.Repository; -using ConnectionsAPI.Models; -using FluentValidation; -using LazyCache; -using System.Text.RegularExpressions; +// using ConnectionsAPI.Database.Repository; +// using ConnectionsAPI.Models; +// using FluentValidation; +// using LazyCache; +// using System.Text.RegularExpressions; -namespace ConnectionsAPI.Features.Puzzle.Get -{ - public record GetPuzzleEndpointRequest(string PuzzleDate); +// namespace ConnectionsAPI.Features.Puzzle.Get +// { +// public record GetPuzzleEndpointRequest(string PuzzleDate); - public partial class GetPuzzleEndpointRequestValidator : Validator - { - [GeneratedRegex("^\\d{4}-\\d{2}-\\d{2}$", RegexOptions.IgnoreCase)] - private static partial Regex PrintDateGeneratedRegex(); +// public partial class GetPuzzleEndpointRequestValidator : Validator +// { +// [GeneratedRegex("^\\d{4}-\\d{2}-\\d{2}$", RegexOptions.IgnoreCase)] +// private static partial Regex PrintDateGeneratedRegex(); - public GetPuzzleEndpointRequestValidator() - { - RuleFor(x => x.PuzzleDate) - .NotEmpty().WithMessage("Puzzle date is required") - .Must(x => PrintDateGeneratedRegex().IsMatch(x)).WithMessage("Puzzle date must be in the format yyyy-MM-dd"); - } - } +// public GetPuzzleEndpointRequestValidator() +// { +// RuleFor(x => x.PuzzleDate) +// .NotEmpty().WithMessage("Puzzle date is required") +// .Must(x => PrintDateGeneratedRegex().IsMatch(x)).WithMessage("Puzzle date must be in the format yyyy-MM-dd"); +// } +// } - public class GetPuzzleEndpoint(PuzzleRepository puzzleRepo, ILogger logger, IAppCache cache) : Endpoint - { - private readonly PuzzleRepository _puzzleRepo = puzzleRepo; - private readonly ILogger _logger = logger; - private readonly IAppCache _cache = cache; +// public class GetPuzzleEndpoint(PuzzleRepository puzzleRepo, ILogger logger, IAppCache cache) : Endpoint +// { +// private readonly PuzzleRepository _puzzleRepo = puzzleRepo; +// private readonly ILogger _logger = logger; +// private readonly IAppCache _cache = cache; - public override void Configure() - { - Get("/{PuzzleDate}.json", - "/puzzle/{PuzzleDate}"); - AllowAnonymous(); - DontThrowIfValidationFails(); - } +// public override void Configure() +// { +// Get("/{PuzzleDate}.json", +// "/puzzle/{PuzzleDate}"); +// AllowAnonymous(); +// DontThrowIfValidationFails(); +// } - public override async Task HandleAsync(GetPuzzleEndpointRequest req, CancellationToken ct) - { - // default to 404 if validation fails - if (ValidationFailed) - { - _logger.LogError("Validation error. {path} {pathBase}", HttpContext.Request.Path, HttpContext.Request.PathBase); - await SendNotFoundAsync(ct); - return; - } +// public override async Task HandleAsync(GetPuzzleEndpointRequest req, CancellationToken ct) +// { +// // default to 404 if validation fails +// if (ValidationFailed) +// { +// _logger.LogError("Validation error. {path} {pathBase}", HttpContext.Request.Path, HttpContext.Request.PathBase); +// await SendNotFoundAsync(ct); +// return; +// } - bool hideSolutions = Query("hideSolutions", isRequired: false); +// bool hideSolutions = Query("hideSolutions", isRequired: false); - // query for the puzzle - var puzzle = await _puzzleRepo.GetPuzzleByDateAsync(req.PuzzleDate, includeSolutions: !hideSolutions); +// // query for the puzzle +// var puzzle = await _puzzleRepo.GetPuzzleByDateAsync(req.PuzzleDate, includeSolutions: !hideSolutions); - // if not found, done here - if (puzzle == null) - { - await SendNotFoundAsync(ct); - return; - } +// // if not found, done here +// if (puzzle == null) +// { +// await SendNotFoundAsync(ct); +// return; +// } - // get response from cache - var response = ConnectionsPuzzleDTO.FromEntity(puzzle); +// // get response from cache +// var response = ConnectionsPuzzleDTO.FromEntity(puzzle); - // done - await SendAsync(response, cancellation: ct); - } - } -} +// // done +// await SendAsync(response, cancellation: ct); +// } +// } +// } diff --git a/Features/Puzzle/List/ListPuzzlesEndpoint.cs b/Features/Puzzle/List/ListPuzzlesEndpoint.cs index de007c8..935c468 100644 --- a/Features/Puzzle/List/ListPuzzlesEndpoint.cs +++ b/Features/Puzzle/List/ListPuzzlesEndpoint.cs @@ -1,34 +1,34 @@ -using ConnectionsAPI.Database.Repository; -using ConnectionsAPI.Models; -using LazyCache; -using Microsoft.EntityFrameworkCore; +// using ConnectionsAPI.Database.Repository; +// using ConnectionsAPI.Models; +// using LazyCache; +// using Microsoft.EntityFrameworkCore; -namespace ConnectionsAPI.Features.Puzzle.List -{ - public class ListPuzzlesEndpoint(PuzzleRepository puzzleRepo, IAppCache cache) : EndpointWithoutRequest> - { - private readonly PuzzleRepository _puzzleRepo = puzzleRepo; - private readonly IAppCache _cache = cache; +// namespace ConnectionsAPI.Features.Puzzle.List +// { +// public class ListPuzzlesEndpoint(PuzzleRepository puzzleRepo, IAppCache cache) : EndpointWithoutRequest> +// { +// private readonly PuzzleRepository _puzzleRepo = puzzleRepo; +// private readonly IAppCache _cache = cache; - public override void Configure() - { - Get("/all.json", - "/puzzle/all"); - AllowAnonymous(); - } +// public override void Configure() +// { +// Get("/all.json", +// "/puzzle/all"); +// AllowAnonymous(); +// } - public override async Task HandleAsync(CancellationToken ct) - { - bool hideSolutions = Query("hideSolutions", isRequired: false); +// public override async Task HandleAsync(CancellationToken ct) +// { +// bool hideSolutions = Query("hideSolutions", isRequired: false); - // query all, ordered by print date - var puzzles = await _puzzleRepo.GetAllPuzzlesAsync(includeSolutions: !hideSolutions); +// // query all, ordered by print date +// var puzzles = await _puzzleRepo.GetAllPuzzlesAsync(includeSolutions: !hideSolutions); - // map to response object - var response = puzzles.Select(ConnectionsPuzzleDTO.FromEntity).ToList(); +// // map to response object +// var response = puzzles.Select(ConnectionsPuzzleDTO.FromEntity).ToList(); - // done - await SendAsync(response, cancellation: ct); - } - } -} +// // done +// await SendAsync(response, cancellation: ct); +// } +// } +// } diff --git a/Models/PuzzleDTO.cs b/Models/PuzzleDTO.cs deleted file mode 100644 index c8f465b..0000000 --- a/Models/PuzzleDTO.cs +++ /dev/null @@ -1,55 +0,0 @@ -namespace ConnectionsAPI.Models -{ - public class ConnectionsPuzzleDTO - { - public static ConnectionsPuzzleDTO FromEntity(Database.Entities.ConnectionsPuzzle dbPuzzle) => - new() - { - PuzzleNumber = dbPuzzle.Index, - PrintDate = dbPuzzle.PrintDate, - Editor = dbPuzzle.EditorName, - - NextPuzzle = dbPuzzle.NextPrintDate ?? string.Empty, - PreviousPuzzle = dbPuzzle.PrevPrintDate ?? string.Empty, - - Categories = dbPuzzle.Categories.OrderBy(x => (int)x.Color).Select(PuzzleCategoryDTO.FromEntity).ToList(), - }; - - public int PuzzleNumber { get; set; } - public string PrintDate { get; set; } = string.Empty; - public string PreviousPuzzle { get; set; } = string.Empty; - public string NextPuzzle { get; set; } = string.Empty; - public string Editor { get; set; } = string.Empty; - public ICollection Categories { get; set; } = []; - } - - public class PuzzleCategoryDTO - { - public static PuzzleCategoryDTO FromEntity(Database.Entities.ConnectionsCategory dbCategory) => - new() - { - Title = dbCategory.Name, - Cards = dbCategory.Cards.OrderBy(x => x.Content).Select(PuzzleCardDTO.FromEntity).ToList(), - Color = dbCategory.Color.ToString().ToLower(), - OrderingKey = (int)dbCategory.Color - }; - - public string Title { get; set; } = string.Empty; - public string Color { get; set; } = string.Empty; - public int OrderingKey { get; set; } - public ICollection Cards { get; set; } = []; - } - - public class PuzzleCardDTO - { - public static PuzzleCardDTO FromEntity(Database.Entities.ConnectionsCard dbCard) => - new() - { - Content = dbCard.Content, - Position = dbCard.Position, - }; - - public string Content { get; set; } = string.Empty; - public int Position { get; set; } - } -} diff --git a/Models/Request/GetPuzzleRequest.cs b/Models/Request/GetPuzzleRequest.cs new file mode 100644 index 0000000..c0fa67e --- /dev/null +++ b/Models/Request/GetPuzzleRequest.cs @@ -0,0 +1,3 @@ +namespace ConnectionsAPI.Models.Request; + +public record GetPuzzleRequest(string PrintDate); diff --git a/Models/Response/ConnectionsPuzzleDTO.cs b/Models/Response/ConnectionsPuzzleDTO.cs new file mode 100644 index 0000000..90e0057 --- /dev/null +++ b/Models/Response/ConnectionsPuzzleDTO.cs @@ -0,0 +1,53 @@ +namespace ConnectionsAPI.Models.Response; +public class ConnectionsPuzzleDTO +{ + public static ConnectionsPuzzleDTO FromEntity(Database.Entities.ConnectionsPuzzle dbPuzzle) => + new() + { + PuzzleNumber = dbPuzzle.Index, + PrintDate = dbPuzzle.PrintDate, + Editor = dbPuzzle.EditorName, + + NextPuzzle = dbPuzzle.NextPrintDate ?? string.Empty, + PreviousPuzzle = dbPuzzle.PrevPrintDate ?? string.Empty, + + Categories = dbPuzzle.Categories.OrderBy(x => (int)x.Color).Select(PuzzleCategoryDTO.FromEntity).ToList(), + }; + + public int PuzzleNumber { get; set; } + public string PrintDate { get; set; } = string.Empty; + public string PreviousPuzzle { get; set; } = string.Empty; + public string NextPuzzle { get; set; } = string.Empty; + public string Editor { get; set; } = string.Empty; + public ICollection Categories { get; set; } = []; +} + +public class PuzzleCategoryDTO +{ + public static PuzzleCategoryDTO FromEntity(Database.Entities.ConnectionsCategory dbCategory) => + new() + { + Title = dbCategory.Name, + Cards = dbCategory.Cards.OrderBy(x => x.Content).Select(PuzzleCardDTO.FromEntity).ToList(), + Color = dbCategory.Color.ToString().ToLower(), + OrderingKey = (int)dbCategory.Color + }; + + public string Title { get; set; } = string.Empty; + public string Color { get; set; } = string.Empty; + public int OrderingKey { get; set; } + public ICollection Cards { get; set; } = []; +} + +public class PuzzleCardDTO +{ + public static PuzzleCardDTO FromEntity(Database.Entities.ConnectionsCard dbCard) => + new() + { + Content = dbCard.Content, + Position = dbCard.Position, + }; + + public string Content { get; set; } = string.Empty; + public int Position { get; set; } +} diff --git a/Validators/GetPuzzleRequestValidator.cs b/Validators/GetPuzzleRequestValidator.cs new file mode 100644 index 0000000..e530597 --- /dev/null +++ b/Validators/GetPuzzleRequestValidator.cs @@ -0,0 +1,23 @@ +using System.Globalization; +using System.Text.RegularExpressions; +using ConnectionsAPI.Models.Request; +using FluentValidation; + +namespace ConnectionsAPI.Validators; + +public partial class GetPuzzleRequestValidator : Validator +{ + [GeneratedRegex("^\\d{4}-\\d{2}-\\d{2}$", RegexOptions.IgnoreCase)] + private static partial Regex PrintDateGeneratedRegex(); + + public GetPuzzleRequestValidator() + { + RuleFor(x => x.PrintDate) + .NotEmpty().WithMessage("Puzzle date is required") + .Must(x => PrintDateGeneratedRegex().IsMatch(x)) + .WithMessage("Print date must be in the format yyyy-MM-dd") + .Must(x => DateTime.TryParseExact(x, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out _)) + .WithMessage("Print date must be a valid datetime"); + + } +}