diff --git a/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Commands/Create/CreateDialogCommandValidator.cs b/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Commands/Create/CreateDialogCommandValidator.cs
index ca76bf84f..b85799516 100644
--- a/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Commands/Create/CreateDialogCommandValidator.cs
+++ b/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Commands/Create/CreateDialogCommandValidator.cs
@@ -31,6 +31,20 @@ public CreateDialogCommandValidator(
.IsValidUuidV7()
.UuidV7TimestampIsInPast();
+ RuleFor(x => x.CreatedAt)
+ .IsInPast();
+
+ RuleFor(x => x.CreatedAt)
+ .NotEmpty()
+ .WithMessage($"{{PropertyName}} must not be empty when '{nameof(CreateDialogCommand.UpdatedAt)} is set.")
+ .When(x => x.UpdatedAt != default);
+
+ RuleFor(x => x.UpdatedAt)
+ .IsInPast()
+ .GreaterThanOrEqualTo(x => x.CreatedAt)
+ .WithMessage($"'{{PropertyName}}' must be greater than or equal to '{nameof(CreateDialogCommand.CreatedAt)}'.")
+ .When(x => x.CreatedAt != default && x.UpdatedAt != default);
+
RuleFor(x => x.ServiceResource)
.NotNull()
.IsValidUri()
diff --git a/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Commands/Create/CreateDialogDto.cs b/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Commands/Create/CreateDialogDto.cs
index 0aaaf1312..036139396 100644
--- a/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Commands/Create/CreateDialogDto.cs
+++ b/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Commands/Create/CreateDialogDto.cs
@@ -95,7 +95,7 @@ public class CreateDialogDto
/// If not supplied, the current date /time will be used.
///
/// 2022-12-31T23:59:59Z
- public DateTimeOffset UpdatedAt { get; set; } // TODO: This does not work, as UpdatedAt is always overwritten
+ public DateTimeOffset UpdatedAt { get; set; }
///
/// The aggregated status of the dialog.
diff --git a/src/Digdir.Library.Entity.Abstractions/Features/Aggregate/AggregateNode.cs b/src/Digdir.Library.Entity.Abstractions/Features/Aggregate/AggregateNode.cs
index 9c729f4f2..297dca3fc 100644
--- a/src/Digdir.Library.Entity.Abstractions/Features/Aggregate/AggregateNode.cs
+++ b/src/Digdir.Library.Entity.Abstractions/Features/Aggregate/AggregateNode.cs
@@ -1,4 +1,5 @@
using System.Reflection;
+using Digdir.Library.Entity.Abstractions.Features.Updatable;
namespace Digdir.Library.Entity.Abstractions.Features.Aggregate;
@@ -88,6 +89,34 @@ internal static AggregateNode Create(Type type, object entity, AggregateNodeStat
/// Convenience method to check if the state of the node is and not by a child node.
///
public bool IsDirectlyModified() => State is AggregateNodeState.Modified && !ModifiedByChild;
+
+ ///
+ /// Convenience method to check if the state of the node is .
+ ///
+ public bool IsAdded() => State is AggregateNodeState.Added;
+
+ ///
+ /// Convenience method to check if the state of the node is .
+ ///
+ public bool IsModified() => State is AggregateNodeState.Modified;
+
+ ///
+ /// Convenience method to check if the state of the node is .
+ ///
+ public bool IsRestored() => State is AggregateNodeState.Restored;
+
+ ///
+ /// Convenience method to check if the state of the node is .
+ ///
+ public bool IsDeleted() => State is AggregateNodeState.Deleted;
+
+ ///
+ /// Convenience method to check if the state of the node is ,
+ /// and that UpdatedAt is the default value.
+ ///
+ ///
+ public bool IsAddedWithDefaultUpdatedAt(IUpdateableEntity updatable)
+ => IsAdded() && updatable.UpdatedAt == default;
}
///
diff --git a/src/Digdir.Library.Entity.EntityFrameworkCore/Features/Aggregate/AggregateExtensions.cs b/src/Digdir.Library.Entity.EntityFrameworkCore/Features/Aggregate/AggregateExtensions.cs
index 6b3c38a78..3a6085906 100644
--- a/src/Digdir.Library.Entity.EntityFrameworkCore/Features/Aggregate/AggregateExtensions.cs
+++ b/src/Digdir.Library.Entity.EntityFrameworkCore/Features/Aggregate/AggregateExtensions.cs
@@ -24,29 +24,32 @@ internal static async Task HandleAggregateEntities(this ChangeTracker changeTrac
foreach (var (_, aggregateNode) in aggregateNodeByEntry)
{
- if (aggregateNode.Entity is IAggregateCreatedHandler created && aggregateNode.State is AggregateNodeState.Added)
+ if (aggregateNode.Entity is IAggregateCreatedHandler created && aggregateNode.IsAdded())
{
created.OnCreate(aggregateNode, utcNow);
}
- if (aggregateNode.Entity is IAggregateUpdatedHandler updated && aggregateNode.State is AggregateNodeState.Modified)
+ if (aggregateNode.Entity is IAggregateUpdatedHandler updated && aggregateNode.IsModified())
{
updated.OnUpdate(aggregateNode, utcNow);
}
- if (aggregateNode.Entity is IAggregateDeletedHandler deleted && aggregateNode.State is AggregateNodeState.Deleted)
+ if (aggregateNode.Entity is IAggregateDeletedHandler deleted && aggregateNode.IsDeleted())
{
deleted.OnDelete(aggregateNode, utcNow);
}
- if (aggregateNode.Entity is IAggregateRestoredHandler restored && aggregateNode.State is AggregateNodeState.Restored)
+ if (aggregateNode.Entity is IAggregateRestoredHandler restored && aggregateNode.IsRestored())
{
restored.OnRestore(aggregateNode, utcNow);
}
if (aggregateNode.Entity is IUpdateableEntity updatable)
{
- updatable.Update(utcNow);
+ if (aggregateNode.IsModified() || aggregateNode.IsAddedWithDefaultUpdatedAt(updatable))
+ {
+ updatable.Update(utcNow);
+ }
}
if (aggregateNode.Entity is IVersionableEntity versionable)
diff --git a/tests/Digdir.Domain.Dialogporten.Application.Integration.Tests/Features/V1/ServiceOwner/Dialogs/Commands/CreateDialogTests.cs b/tests/Digdir.Domain.Dialogporten.Application.Integration.Tests/Features/V1/ServiceOwner/Dialogs/Commands/CreateDialogTests.cs
index 795c643cd..db17226ed 100644
--- a/tests/Digdir.Domain.Dialogporten.Application.Integration.Tests/Features/V1/ServiceOwner/Dialogs/Commands/CreateDialogTests.cs
+++ b/tests/Digdir.Domain.Dialogporten.Application.Integration.Tests/Features/V1/ServiceOwner/Dialogs/Commands/CreateDialogTests.cs
@@ -1,4 +1,5 @@
-using Digdir.Domain.Dialogporten.Application.Integration.Tests.Common;
+using Digdir.Domain.Dialogporten.Application.Features.V1.ServiceOwner.Dialogs.Queries.Get;
+using Digdir.Domain.Dialogporten.Application.Integration.Tests.Common;
using Digdir.Tool.Dialogporten.GenerateFakeData;
using FluentAssertions;
using static Digdir.Domain.Dialogporten.Application.Integration.Tests.UuiDv7Utils;
@@ -108,5 +109,98 @@ public async Task Create_CreateDialog_WhenDialogIsComplex()
success.Value.Should().Be(expectedDialogId);
}
- // TODO: Add tests
+ [Fact]
+ public async Task Can_Create_Dialog_With_UpdatedAt_Supplied()
+ {
+ // Arrange
+ var dialogId = GenerateBigEndianUuidV7();
+ var createdAt = DateTimeOffset.UtcNow.AddYears(-20);
+ var updatedAt = DateTimeOffset.UtcNow.AddYears(-15);
+ var createDialogCommand = DialogGenerator.GenerateFakeDialog(id: dialogId, updatedAt: updatedAt, createdAt: createdAt);
+
+ // Act
+ var createDialogResult = await Application.Send(createDialogCommand);
+ var getDialogQuery = new GetDialogQuery
+ {
+ DialogId = dialogId
+ };
+
+ var getDialogResponse = await Application.Send(getDialogQuery);
+
+ // Assert
+ createDialogResult.TryPickT0(out var dialogCreatedSuccess, out _).Should().BeTrue();
+ dialogCreatedSuccess.Value.Should().Be(dialogId);
+
+ getDialogQuery.Should().NotBeNull();
+ getDialogResponse.TryPickT0(out var dialog, out _).Should().BeTrue();
+ dialog.Should().NotBeNull();
+ dialog.CreatedAt.Should().BeCloseTo(createdAt, precision: TimeSpan.FromMicroseconds(10));
+ dialog.UpdatedAt.Should().BeCloseTo(updatedAt, precision: TimeSpan.FromMicroseconds(10));
+ }
+
+ [Fact]
+ public async Task Cant_Create_Dialog_With_UpdatedAt_Supplied_Without_CreatedAt_Supplied()
+ {
+ // Arrange
+ var updatedAt = DateTimeOffset.UtcNow.AddYears(-15);
+ var createDialogCommand = DialogGenerator.GenerateFakeDialog(updatedAt: updatedAt);
+
+ // Act
+ var response = await Application.Send(createDialogCommand);
+
+ // Assert
+ response.TryPickT2(out var validationError, out _).Should().BeTrue();
+ validationError.Should().NotBeNull();
+ validationError.Errors.Should().Contain(e => e.ErrorMessage.Contains(nameof(createDialogCommand.UpdatedAt)));
+ }
+
+ [Fact]
+ public async Task Cant_Create_Dialog_With_UpdatedAt_Date_Earlier_Than_CreatedAt_Date()
+ {
+ // Arrange
+ var createdAt = DateTimeOffset.UtcNow.AddYears(-10);
+ var updatedAt = DateTimeOffset.UtcNow.AddYears(-15);
+ var createDialogCommand = DialogGenerator.GenerateFakeDialog(updatedAt: updatedAt, createdAt: createdAt);
+
+ // Act
+ var response = await Application.Send(createDialogCommand);
+
+ // Assert
+ response.TryPickT2(out var validationError, out _).Should().BeTrue();
+ validationError.Should().NotBeNull();
+ validationError.Errors.Should().Contain(e => e.ErrorMessage.Contains(nameof(createDialogCommand.CreatedAt)));
+ }
+
+ [Fact]
+ public async Task Cant_Create_Dialog_With_UpdatedAt_Or_CreatedAt_In_The_Future()
+ {
+ // Arrange
+ var aYearFromNow = DateTimeOffset.UtcNow.AddYears(1);
+ var createDialogCommand = DialogGenerator
+ .GenerateFakeDialog(updatedAt: aYearFromNow, createdAt: aYearFromNow);
+
+ // Act
+ var response = await Application.Send(createDialogCommand);
+
+ //
+ response.TryPickT2(out var validationError, out _).Should().BeTrue();
+ validationError.Should().NotBeNull();
+ validationError.Errors.Should().Contain(e => e.ErrorMessage.Contains("in the past"));
+ }
+
+ [Fact]
+ public async Task Can_Create_Dialog_With_UpdatedAt_And_CreatedAt_Being_Equal()
+ {
+ // Arrange
+ var aYearAgo = DateTimeOffset.UtcNow.AddYears(-1);
+ var createDialogCommand = DialogGenerator
+ .GenerateFakeDialog(updatedAt: aYearAgo, createdAt: aYearAgo);
+
+ // Act
+ var response = await Application.Send(createDialogCommand);
+
+ // Assert
+ response.TryPickT0(out var success, out _).Should().BeTrue();
+ success.Should().NotBeNull();
+ }
}