diff --git a/FIleProcessor.Models/FileProfile.cs b/FIleProcessor.Models/FileProfile.cs
index f75194e..2559782 100644
--- a/FIleProcessor.Models/FileProfile.cs
+++ b/FIleProcessor.Models/FileProfile.cs
@@ -16,6 +16,10 @@ public record FileProfile
///
public Guid FileProfileId { get; init; }
+ public Guid FileProfileVersionId { get; init; }
+
+ public Int32 FileProfileVersion { get; init; }
+
///
/// Gets or sets the name.
///
@@ -85,9 +89,19 @@ public record FileProfile
/// Type of the request.
/// Name of the operator.
/// The line terminator.
- public FileProfile(Guid fileProfileId, String name, String listeningDirectory, String requestType, String operatorName, String lineTerminator, String fileFormatHandler)
+ public FileProfile(Guid fileProfileId,
+ String name,
+ String listeningDirectory,
+ String requestType,
+ String operatorName,
+ String lineTerminator,
+ String fileFormatHandler,
+ Guid fileProfileVersionId = default,
+ Int32 fileProfileVersion = 0)
{
this.FileProfileId = fileProfileId;
+ this.FileProfileVersionId = fileProfileVersionId;
+ this.FileProfileVersion = fileProfileVersion;
this.Name = name;
this.ListeningDirectory = listeningDirectory;
this.ProcessedDirectory = $"{listeningDirectory}/processed";
@@ -98,4 +112,4 @@ public FileProfile(Guid fileProfileId, String name, String listeningDirectory, S
this.FileFormatHandler = fileFormatHandler;
}
}
-}
\ No newline at end of file
+}
diff --git a/FileProcessor.BusinessLogic/Managers/FileProfileManager.cs b/FileProcessor.BusinessLogic/Managers/FileProfileManager.cs
index 9ffc7cf..518f50f 100644
--- a/FileProcessor.BusinessLogic/Managers/FileProfileManager.cs
+++ b/FileProcessor.BusinessLogic/Managers/FileProfileManager.cs
@@ -52,6 +52,34 @@ public async Task> GetFileProfile(Guid fileProfileId, C
return Result.Success(fileProfile);
}
+ public async Task>> GetFileProfileVersions(Guid fileProfileId, CancellationToken cancellationToken)
+ {
+ Result aggregateResult = await this.LoadAggregate(cancellationToken);
+ if (aggregateResult.IsFailed)
+ return ResultHelpers.CreateFailure(aggregateResult);
+
+ List versions = aggregateResult.Data.GetProfileVersions(fileProfileId);
+ if (versions.Count == 0)
+ return Result.NotFound($"No file profile found for File Profile Id {fileProfileId}");
+
+ return Result.Success(versions);
+ }
+
+ public async Task> GetFileProfileVersion(Guid fileProfileId,
+ Guid fileProfileVersionId,
+ CancellationToken cancellationToken)
+ {
+ Result aggregateResult = await this.LoadAggregate(cancellationToken);
+ if (aggregateResult.IsFailed)
+ return ResultHelpers.CreateFailure(aggregateResult);
+
+ FileProfileModel version = aggregateResult.Data.GetProfileVersion(fileProfileId, fileProfileVersionId);
+ if (version == null)
+ return Result.NotFound($"No file profile version found for File Profile Id {fileProfileId} and Version Id {fileProfileVersionId}");
+
+ return Result.Success(version);
+ }
+
public async Task> CreateFileProfile(CreateFileProfileRequest request, CancellationToken cancellationToken)
{
Result aggregateResult = await this.LoadAggregate(cancellationToken);
diff --git a/FileProcessor.BusinessLogic/Managers/IFileProfileManager.cs b/FileProcessor.BusinessLogic/Managers/IFileProfileManager.cs
index a5ff4ce..55e1c17 100644
--- a/FileProcessor.BusinessLogic/Managers/IFileProfileManager.cs
+++ b/FileProcessor.BusinessLogic/Managers/IFileProfileManager.cs
@@ -14,6 +14,10 @@ public interface IFileProfileManager
Task> GetFileProfile(Guid fileProfileId, CancellationToken cancellationToken);
+ Task>> GetFileProfileVersions(Guid fileProfileId, CancellationToken cancellationToken);
+
+ Task> GetFileProfileVersion(Guid fileProfileId, Guid fileProfileVersionId, CancellationToken cancellationToken);
+
Task> CreateFileProfile(CreateFileProfileRequest request, CancellationToken cancellationToken);
Task> UpdateFileProfile(Guid fileProfileId, UpdateFileProfileRequest request, CancellationToken cancellationToken);
diff --git a/FileProcessor.FileProfile.DomainEvents/FileProfileDomainEvents.cs b/FileProcessor.FileProfile.DomainEvents/FileProfileDomainEvents.cs
index 917e6c7..b7da151 100644
--- a/FileProcessor.FileProfile.DomainEvents/FileProfileDomainEvents.cs
+++ b/FileProcessor.FileProfile.DomainEvents/FileProfileDomainEvents.cs
@@ -10,7 +10,20 @@ public record FileProfileCreatedEvent(Guid FileProfileCollectionId,
String RequestType,
String OperatorName,
String LineTerminator,
- String FileFormatHandler) : DomainEvent(FileProfileCollectionId, Guid.NewGuid());
+ String FileFormatHandler,
+ Guid FileProfileVersionId = default,
+ Int32 FileProfileVersion = 1) : DomainEvent(FileProfileCollectionId, Guid.NewGuid());
+
+public record FileProfileVersionCreatedEvent(Guid FileProfileCollectionId,
+ Guid FileProfileId,
+ Guid FileProfileVersionId,
+ Int32 FileProfileVersion,
+ String Name,
+ String ListeningDirectory,
+ String RequestType,
+ String OperatorName,
+ String LineTerminator,
+ String FileFormatHandler) : DomainEvent(FileProfileCollectionId, Guid.NewGuid());
public record FileProfileNameUpdatedEvent(Guid FileProfileCollectionId, Guid FileProfileId, String Name) : DomainEvent(FileProfileCollectionId, Guid.NewGuid());
diff --git a/FileProcessor.FileProfileAggregate.Tests/FileProfileAggregateTests.cs b/FileProcessor.FileProfileAggregate.Tests/FileProfileAggregateTests.cs
index 49b8c14..ae3367f 100644
--- a/FileProcessor.FileProfileAggregate.Tests/FileProfileAggregateTests.cs
+++ b/FileProcessor.FileProfileAggregate.Tests/FileProfileAggregateTests.cs
@@ -171,6 +171,95 @@ public void FileProfileAggregate_Update_ListeningDirectoryUpdatesDerivedDirector
updatedProfile.FailedDirectory.ShouldBe("/home/txnproc/bulkfiles/new-safaricom/failed");
}
+ [Fact]
+ public void FileProfileAggregate_Create_CreatesInitialImmutableVersion()
+ {
+ FileProfileAggregateRoot aggregate = FileProfileAggregateRoot.Create();
+
+ Result createResult = aggregate.CreateProfile(this.CreateProfile(
+ TestData.FileProfileId,
+ TestData.SafaricomProfileName,
+ TestData.SafaricomListeningDirectory,
+ TestData.SafaricomRequestType,
+ TestData.SafaricomOperatorIdentifier,
+ LineTerminatorType.LineFeed,
+ TestData.SafaricomFileFormatHandler));
+
+ FileProcessor.Models.FileProfile profile = aggregate.GetProfile(TestData.FileProfileId);
+ List versions = aggregate.GetProfileVersions(TestData.FileProfileId);
+
+ createResult.IsSuccess.ShouldBeTrue();
+ profile.FileProfileVersionId.ShouldNotBe(Guid.Empty);
+ profile.FileProfileVersion.ShouldBe(1);
+ versions.Count.ShouldBe(1);
+ versions[0].FileProfileVersionId.ShouldBe(profile.FileProfileVersionId);
+ }
+
+ [Fact]
+ public void FileProfileAggregate_Update_CreatesNewVersionAndPreservesPreviousVersion()
+ {
+ FileProfileAggregateRoot aggregate = FileProfileAggregateRoot.Create();
+ aggregate.CreateProfile(this.CreateProfile(
+ TestData.FileProfileId,
+ TestData.SafaricomProfileName,
+ TestData.SafaricomListeningDirectory,
+ TestData.SafaricomRequestType,
+ TestData.SafaricomOperatorIdentifier,
+ LineTerminatorType.LineFeed,
+ TestData.SafaricomFileFormatHandler));
+
+ FileProcessor.Models.FileProfile originalProfile = aggregate.GetProfile(TestData.FileProfileId);
+
+ Result updateResult = aggregate.UpdateProfile(TestData.FileProfileId, new UpdateFileProfileRequest
+ {
+ FileFormatHandler = "UpdatedHandler"
+ });
+
+ FileProcessor.Models.FileProfile updatedProfile = aggregate.GetProfile(TestData.FileProfileId);
+ FileProcessor.Models.FileProfile historicalProfile = aggregate.GetProfileVersion(
+ TestData.FileProfileId,
+ originalProfile.FileProfileVersionId);
+ List versions = aggregate.GetProfileVersions(TestData.FileProfileId);
+
+ updateResult.IsSuccess.ShouldBeTrue();
+ updatedProfile.FileProfileVersionId.ShouldNotBe(originalProfile.FileProfileVersionId);
+ updatedProfile.FileProfileVersion.ShouldBe(2);
+ updatedProfile.FileFormatHandler.ShouldBe("UpdatedHandler");
+ historicalProfile.FileProfileVersionId.ShouldBe(originalProfile.FileProfileVersionId);
+ historicalProfile.FileProfileVersion.ShouldBe(1);
+ historicalProfile.FileFormatHandler.ShouldBe(TestData.SafaricomFileFormatHandler);
+ versions.Count.ShouldBe(2);
+ }
+
+ [Fact]
+ public void FileProfileAggregate_ReplayingLegacyUpdateEvents_UpdatesInitialVersionSnapshot()
+ {
+ FileProfileAggregateRoot aggregate = FileProfileAggregateRoot.Create();
+ Guid versionId = Guid.Parse("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa");
+
+ aggregate.PlayEvent(new FileProcessor.FileProfile.DomainEvents.FileProfileCreatedEvent(
+ FileProfileAggregateRoot.FileProfileCollectionId,
+ TestData.FileProfileId,
+ TestData.SafaricomProfileName,
+ TestData.SafaricomListeningDirectory,
+ TestData.SafaricomRequestType,
+ TestData.SafaricomOperatorIdentifier,
+ "\n",
+ TestData.SafaricomFileFormatHandler,
+ versionId,
+ 1));
+ aggregate.PlayEvent(new FileProcessor.FileProfile.DomainEvents.FileProfileFileFormatHandlerUpdatedEvent(
+ FileProfileAggregateRoot.FileProfileCollectionId,
+ TestData.FileProfileId,
+ "LegacyUpdatedHandler"));
+
+ FileProcessor.Models.FileProfile profile = aggregate.GetProfile(TestData.FileProfileId);
+ FileProcessor.Models.FileProfile version = aggregate.GetProfileVersion(TestData.FileProfileId, versionId);
+
+ profile.FileFormatHandler.ShouldBe("LegacyUpdatedHandler");
+ version.FileFormatHandler.ShouldBe("LegacyUpdatedHandler");
+ }
+
[Fact]
public void FileProfileAggregate_Create_WithDuplicateName_IsRejected()
{
diff --git a/FileProcessor.FileProfileAggregate/FileProfileAggregate.cs b/FileProcessor.FileProfileAggregate/FileProfileAggregate.cs
index 4605978..8c3d0ad 100644
--- a/FileProcessor.FileProfileAggregate/FileProfileAggregate.cs
+++ b/FileProcessor.FileProfileAggregate/FileProfileAggregate.cs
@@ -30,44 +30,92 @@ public static void PlayEvent(this FileProfileAggregate aggregate, FileProfileCre
OperatorName = domainEvent.OperatorName,
LineTerminator = domainEvent.LineTerminator,
FileFormatHandler = domainEvent.FileFormatHandler,
- IsArchived = false
+ IsArchived = false,
+ CurrentVersionId = domainEvent.FileProfileVersionId == Guid.Empty ? domainEvent.FileProfileId : domainEvent.FileProfileVersionId,
+ CurrentVersion = domainEvent.FileProfileVersion,
+ Versions = new List
+ {
+ new()
+ {
+ FileProfileVersionId = domainEvent.FileProfileVersionId == Guid.Empty ? domainEvent.FileProfileId : domainEvent.FileProfileVersionId,
+ FileProfileVersion = domainEvent.FileProfileVersion,
+ Name = domainEvent.Name,
+ ListeningDirectory = domainEvent.ListeningDirectory,
+ RequestType = domainEvent.RequestType,
+ OperatorName = domainEvent.OperatorName,
+ LineTerminator = domainEvent.LineTerminator,
+ FileFormatHandler = domainEvent.FileFormatHandler
+ }
+ }
+ };
+ }
+
+ public static void PlayEvent(this FileProfileAggregate aggregate, FileProfileVersionCreatedEvent domainEvent)
+ {
+ FileProfileState fileProfile = aggregate.GetRequiredProfileState(domainEvent.FileProfileId);
+ FileProfileVersionState version = new()
+ {
+ FileProfileVersionId = domainEvent.FileProfileVersionId,
+ FileProfileVersion = domainEvent.FileProfileVersion,
+ Name = domainEvent.Name,
+ ListeningDirectory = domainEvent.ListeningDirectory,
+ RequestType = domainEvent.RequestType,
+ OperatorName = domainEvent.OperatorName,
+ LineTerminator = domainEvent.LineTerminator,
+ FileFormatHandler = domainEvent.FileFormatHandler
};
+
+ fileProfile.Versions.Add(version);
+ fileProfile.CurrentVersionId = version.FileProfileVersionId;
+ fileProfile.CurrentVersion = version.FileProfileVersion;
+ fileProfile.Name = version.Name;
+ fileProfile.ListeningDirectory = version.ListeningDirectory;
+ fileProfile.RequestType = version.RequestType;
+ fileProfile.OperatorName = version.OperatorName;
+ fileProfile.LineTerminator = version.LineTerminator;
+ fileProfile.FileFormatHandler = version.FileFormatHandler;
}
public static void PlayEvent(this FileProfileAggregate aggregate, FileProfileNameUpdatedEvent domainEvent)
{
FileProfileState fileProfile = aggregate.GetRequiredProfileState(domainEvent.FileProfileId);
fileProfile.Name = domainEvent.Name;
+ fileProfile.UpdateCurrentVersionSnapshot();
}
public static void PlayEvent(this FileProfileAggregate aggregate, FileProfileListeningDirectoryUpdatedEvent domainEvent)
{
FileProfileState fileProfile = aggregate.GetRequiredProfileState(domainEvent.FileProfileId);
fileProfile.ListeningDirectory = domainEvent.ListeningDirectory;
+ fileProfile.UpdateCurrentVersionSnapshot();
}
public static void PlayEvent(this FileProfileAggregate aggregate, FileProfileRequestTypeUpdatedEvent domainEvent)
{
FileProfileState fileProfile = aggregate.GetRequiredProfileState(domainEvent.FileProfileId);
fileProfile.RequestType = domainEvent.RequestType;
+ fileProfile.UpdateCurrentVersionSnapshot();
}
public static void PlayEvent(this FileProfileAggregate aggregate, FileProfileOperatorNameUpdatedEvent domainEvent)
{
FileProfileState fileProfile = aggregate.GetRequiredProfileState(domainEvent.FileProfileId);
fileProfile.OperatorName = domainEvent.OperatorName;
+ fileProfile.UpdateCurrentVersionSnapshot();
}
public static void PlayEvent(this FileProfileAggregate aggregate, FileProfileLineTerminatorUpdatedEvent domainEvent)
{
FileProfileState fileProfile = aggregate.GetRequiredProfileState(domainEvent.FileProfileId);
fileProfile.LineTerminator = domainEvent.LineTerminator;
+ fileProfile.UpdateCurrentVersionSnapshot();
}
public static void PlayEvent(this FileProfileAggregate aggregate, FileProfileFileFormatHandlerUpdatedEvent domainEvent)
{
FileProfileState fileProfile = aggregate.GetRequiredProfileState(domainEvent.FileProfileId);
fileProfile.FileFormatHandler = domainEvent.FileFormatHandler;
+ fileProfile.UpdateCurrentVersionSnapshot();
}
public static void PlayEvent(this FileProfileAggregate aggregate, FileProfileArchivedEvent domainEvent)
@@ -105,7 +153,9 @@ public static Result CreateProfile(this FileProfileAggregate aggregate, CreateFi
request.RequestType.Trim(),
request.OperatorName.Trim(),
lineTerminatorResult.Data,
- request.FileFormatHandler.Trim());
+ request.FileFormatHandler.Trim(),
+ Guid.NewGuid(),
+ 1);
aggregate.ApplyAndAppend(fileProfileCreatedEvent);
return Result.Success();
@@ -124,37 +174,44 @@ public static Result UpdateProfile(this FileProfileAggregate aggregate, Guid fil
return validationResult;
}
- if (IsDomainEventRequired(profile.Name, request.Name)) {
- aggregate.ApplyAndAppend(new FileProfileNameUpdatedEvent(aggregate.AggregateId, fileProfileId, request.Name));
- }
-
- if (IsDomainEventRequired(profile.ListeningDirectory, request.ListeningDirectory)) {
- aggregate.ApplyAndAppend(new FileProfileListeningDirectoryUpdatedEvent(aggregate.AggregateId, fileProfileId, request.ListeningDirectory));
- }
-
- if (IsDomainEventRequired(profile.RequestType, request.RequestType)) {
- aggregate.ApplyAndAppend(new FileProfileRequestTypeUpdatedEvent(aggregate.AggregateId, fileProfileId, request.RequestType));
- }
-
- if (IsDomainEventRequired(profile.OperatorName, request.OperatorName)) {
- aggregate.ApplyAndAppend(new FileProfileOperatorNameUpdatedEvent(aggregate.AggregateId, fileProfileId, request.OperatorName));
- }
-
+ String lineTerminator = profile.LineTerminator;
if (request.LineTerminator.HasValue) {
Result lineTerminatorResult = request.LineTerminator.Value.ToLineTerminator();
if (lineTerminatorResult.IsFailed) {
return ResultHelpers.CreateFailure(lineTerminatorResult);
}
- if (IsDomainEventRequired(profile.LineTerminator, lineTerminatorResult.Data)) {
- aggregate.ApplyAndAppend(new FileProfileLineTerminatorUpdatedEvent(aggregate.AggregateId, fileProfileId, lineTerminatorResult.Data));
- }
+ lineTerminator = lineTerminatorResult.Data;
}
- if (IsDomainEventRequired(profile.FileFormatHandler, request.FileFormatHandler)) {
- aggregate.ApplyAndAppend(new FileProfileFileFormatHandlerUpdatedEvent(aggregate.AggregateId, fileProfileId, request.FileFormatHandler));
+ String name = request.Name == null ? profile.Name : request.Name.Trim();
+ String listeningDirectory = request.ListeningDirectory == null ? profile.ListeningDirectory : request.ListeningDirectory.Trim();
+ String requestType = request.RequestType == null ? profile.RequestType : request.RequestType.Trim();
+ String operatorName = request.OperatorName == null ? profile.OperatorName : request.OperatorName.Trim();
+ String fileFormatHandler = request.FileFormatHandler == null ? profile.FileFormatHandler : request.FileFormatHandler.Trim();
+
+ if (IsDomainEventRequired(profile.Name, name) == false &&
+ IsDomainEventRequired(profile.ListeningDirectory, listeningDirectory) == false &&
+ IsDomainEventRequired(profile.RequestType, requestType) == false &&
+ IsDomainEventRequired(profile.OperatorName, operatorName) == false &&
+ IsDomainEventRequired(profile.LineTerminator, lineTerminator) == false &&
+ IsDomainEventRequired(profile.FileFormatHandler, fileFormatHandler) == false)
+ {
+ return Result.Success();
}
+ Int32 nextVersion = profile.Versions.Count == 0 ? 1 : profile.Versions.Max(version => version.FileProfileVersion) + 1;
+ aggregate.ApplyAndAppend(new FileProfileVersionCreatedEvent(aggregate.AggregateId,
+ fileProfileId,
+ Guid.NewGuid(),
+ nextVersion,
+ name,
+ listeningDirectory,
+ requestType,
+ operatorName,
+ lineTerminator,
+ fileFormatHandler));
+
return Result.Success();
}
@@ -232,6 +289,27 @@ public static FileProfileModel GetProfile(this FileProfileAggregate aggregate, G
return profile.ToModel();
}
+ public static List GetProfileVersions(this FileProfileAggregate aggregate, Guid fileProfileId)
+ {
+ if (aggregate.FileProfiles.TryGetValue(fileProfileId, out FileProfileState profile) == false)
+ return new List();
+
+ return profile.Versions.OrderBy(version => version.FileProfileVersion)
+ .Select(version => version.ToModel(fileProfileId))
+ .ToList();
+ }
+
+ public static FileProfileModel GetProfileVersion(this FileProfileAggregate aggregate,
+ Guid fileProfileId,
+ Guid fileProfileVersionId)
+ {
+ if (aggregate.FileProfiles.TryGetValue(fileProfileId, out FileProfileState profile) == false)
+ return null;
+
+ FileProfileVersionState version = profile.Versions.SingleOrDefault(candidate => candidate.FileProfileVersionId == fileProfileVersionId);
+ return version?.ToModel(fileProfileId);
+ }
+
private static Result ValidateCreateRequest(this FileProfileAggregate aggregate, CreateFileProfileRequest request)
{
if (request == null)
@@ -439,7 +517,33 @@ private static FileProfileModel ToModel(this FileProfileState fileProfile)
fileProfile.RequestType,
fileProfile.OperatorName,
fileProfile.LineTerminator,
- fileProfile.FileFormatHandler);
+ fileProfile.FileFormatHandler,
+ fileProfile.CurrentVersionId,
+ fileProfile.CurrentVersion);
+ }
+
+ private static FileProfileModel ToModel(this FileProfileVersionState version, Guid fileProfileId)
+ {
+ return new FileProfileModel(fileProfileId,
+ version.Name,
+ version.ListeningDirectory,
+ version.RequestType,
+ version.OperatorName,
+ version.LineTerminator,
+ version.FileFormatHandler,
+ version.FileProfileVersionId,
+ version.FileProfileVersion);
+ }
+
+ private static void UpdateCurrentVersionSnapshot(this FileProfileState fileProfile)
+ {
+ FileProfileVersionState currentVersion = fileProfile.Versions.Single(version => version.FileProfileVersionId == fileProfile.CurrentVersionId);
+ currentVersion.Name = fileProfile.Name;
+ currentVersion.ListeningDirectory = fileProfile.ListeningDirectory;
+ currentVersion.RequestType = fileProfile.RequestType;
+ currentVersion.OperatorName = fileProfile.OperatorName;
+ currentVersion.LineTerminator = fileProfile.LineTerminator;
+ currentVersion.FileFormatHandler = fileProfile.FileFormatHandler;
}
}
@@ -509,4 +613,29 @@ internal sealed class FileProfileState
public string FileFormatHandler { get; set; }
public bool IsArchived { get; set; }
+
+ public Guid CurrentVersionId { get; set; }
+
+ public int CurrentVersion { get; set; }
+
+ public List Versions { get; set; } = new();
+}
+
+internal sealed class FileProfileVersionState
+{
+ public Guid FileProfileVersionId { get; set; }
+
+ public int FileProfileVersion { get; set; }
+
+ public string Name { get; set; }
+
+ public string ListeningDirectory { get; set; }
+
+ public string RequestType { get; set; }
+
+ public string OperatorName { get; set; }
+
+ public string LineTerminator { get; set; }
+
+ public string FileFormatHandler { get; set; }
}