From 33419d38b0b8bb83a334c8763b5495a77d932e31 Mon Sep 17 00:00:00 2001
From: jmayer913 <72579603+jmayer913@users.noreply.github.com>
Date: Sat, 26 Jul 2025 13:47:42 -0400
Subject: [PATCH 1/4] Updated the Version
---
JMayer.Data/JMayer.Data.csproj | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/JMayer.Data/JMayer.Data.csproj b/JMayer.Data/JMayer.Data.csproj
index 27cbb04..b89b7a0 100644
--- a/JMayer.Data/JMayer.Data.csproj
+++ b/JMayer.Data/JMayer.Data.csproj
@@ -4,7 +4,7 @@
net8.0
enable
enable
- 1.2.2
+ 1.3.0
jmayer913
jmayer913
Package contains generic data access via HTTP or a Database.
From 4c629c50ca14c77377fdd3c2e085cc3cc639a627 Mon Sep 17 00:00:00 2001
From: jmayer913 <72579603+jmayer913@users.noreply.github.com>
Date: Sat, 26 Jul 2025 13:56:17 -0400
Subject: [PATCH 2/4] New Properties for the UserEditableDataLayer
The ability to enable/disable the update data conflict detection. The ability to enable/disable the precision of the timestamp comparison for the update data conflict detection.
---
.../DataLayer/IUserEditableDataLayer.cs | 25 +++++++++
.../MemoryStorage/UserEditableDataLayer.cs | 55 ++++++++++++++-----
2 files changed, 65 insertions(+), 15 deletions(-)
diff --git a/JMayer.Data/Database/DataLayer/IUserEditableDataLayer.cs b/JMayer.Data/Database/DataLayer/IUserEditableDataLayer.cs
index 467b363..2997971 100644
--- a/JMayer.Data/Database/DataLayer/IUserEditableDataLayer.cs
+++ b/JMayer.Data/Database/DataLayer/IUserEditableDataLayer.cs
@@ -10,6 +10,31 @@ namespace JMayer.Data.Database.DataLayer;
/// A UserEditableDataObject which represents the user editable data in the collection/table.
public interface IUserEditableDataLayer : IStandardCRUDDataLayer where T : UserEditableDataObject
{
+ ///
+ /// The property gets/sets if milliseconds, microseconds and nanoseconds are ignored when comparing the LastEditedOn
+ /// for old data object detection.
+ ///
+ ///
+ /// Syncfusion converts all DateTime properties in an object to UTC and the millseconds, microseconds and nanoseconds
+ /// are stipped from the DateTime. Because of this, the data layer would always detect old data and throw a DataObjectUpdateConflictException
+ /// so this property was introduced when the comparison needs to be less precise.
+ ///
+ /// This property is ignored if the IsOldDataObjectDetectionEnabled set to false.
+ ///
+ bool IsLessPreciseTimestampComparisonEnabled { get; set; }
+
+ ///
+ /// The property gets/sets if the data layer checks if the data object being updated is considered old.
+ ///
+ ///
+ /// When enabled, the data layer will compare the data object passed into UpdateAsync() with the data
+ /// object already stored in the collection/table. The LastEditedOn property will be compared and if the
+ /// timestamps are the same then no one else has edited the data object and the update will occur. If the
+ /// timestamps are not the same, another user has edited the data object and the data layer throws a
+ /// DataObjectUpdateConflictException.
+ ///
+ bool IsOldDataObjectDetectionEnabled { get; set; }
+
///
/// The method returns all the data objects for the collection/table as a list view based on a where predicate with an order.
///
diff --git a/JMayer.Data/Database/DataLayer/MemoryStorage/UserEditableDataLayer.cs b/JMayer.Data/Database/DataLayer/MemoryStorage/UserEditableDataLayer.cs
index aaac505..2a3e31b 100644
--- a/JMayer.Data/Database/DataLayer/MemoryStorage/UserEditableDataLayer.cs
+++ b/JMayer.Data/Database/DataLayer/MemoryStorage/UserEditableDataLayer.cs
@@ -16,23 +16,52 @@ namespace JMayer.Data.Database.DataLayer.MemoryStorage;
/// Also, the underlying data storage is a List so this shouldn't be used with very large datasets.
///
///
-/// UpdateAsync() has conflict detection. Because the UserEditableDataObject has a LastEditedOn property,
-/// update can now determine if the data object passed in is older than the record in memory. When this occurs,
-/// an exception is thrown with the idea the user will need to refresh the data to have the latest and then
-/// try editing it again.
+/// By default, old data detection is enabled. UpdateAsync() will use the LastEditedOn property to determine
+/// if the data object passed in is older than the data object in memory. When a conflict is detected, an exception is thrown
+/// with the idea the user will need to refresh the data to have the latest and then try editing it again.
///
///
public class UserEditableDataLayer : StandardCRUDDataLayer, IUserEditableDataLayer where T : UserEditableDataObject, new()
{
+ ///
+ public bool IsLessPreciseTimestampComparisonEnabled { get; set; }
+
+ ///
+ public bool IsOldDataObjectDetectionEnabled { get; set; } = true;
+
///
/// The method returns if there's no update conflict.
///
/// The data object in the database.
/// The data object updated by the user.
/// True means the data object can be updated; false means there's a conflict.
- private static bool AllowToUpdate(T databaseDataObject, T userDataObject)
+ private bool AllowToUpdate(T databaseDataObject, T userDataObject)
{
- return databaseDataObject.LastEditedOn == userDataObject.LastEditedOn;
+ if (IsLessPreciseTimestampComparisonEnabled)
+ {
+ //If both are not null, recreate the LastEditedOn but without the millseconds, microseconds & nanoseconds and then, compare to two.
+ if (databaseDataObject.LastEditedOn is not null && userDataObject.LastEditedOn is not null)
+ {
+ DateTime databaseDataObjectLastEditedOn = new(databaseDataObject.LastEditedOn.Value.Year, databaseDataObject.LastEditedOn.Value.Month, databaseDataObject.LastEditedOn.Value.Day, databaseDataObject.LastEditedOn.Value.Hour, databaseDataObject.LastEditedOn.Value.Minute, databaseDataObject.LastEditedOn.Value.Second);
+ DateTime userDataObjectLastEditedOn = new(userDataObject.LastEditedOn.Value.Year, userDataObject.LastEditedOn.Value.Month, userDataObject.LastEditedOn.Value.Day, userDataObject.LastEditedOn.Value.Hour, userDataObject.LastEditedOn.Value.Minute, userDataObject.LastEditedOn.Value.Second);
+
+ return databaseDataObjectLastEditedOn == userDataObjectLastEditedOn;
+ }
+ //If both null then no edits have occurred so allow the update.
+ else if (databaseDataObject.LastEditedOn is null && userDataObject.LastEditedOn is null)
+ {
+ return true;
+ }
+ //If one is null and the other not null then edits have occurred so do not allow an update.
+ else
+ {
+ return false;
+ }
+ }
+ else
+ {
+ return databaseDataObject.LastEditedOn == userDataObject.LastEditedOn;
+ }
}
///
@@ -112,14 +141,10 @@ public async override Task> UpdateAsync(List dataObjects, Cancellatio
//Confirm the ID exists and the data object isn't old because this wants all or none updated.
foreach (T dataObject in dataObjects)
{
- T? databaseDataObject = DataStorage.FirstOrDefault(obj => obj.Integer64ID == dataObject.Integer64ID);
-
- if (databaseDataObject == null)
- {
- throw new IDNotFoundException(dataObject.Integer64ID.ToString());
- }
+ T? databaseDataObject = DataStorage.FirstOrDefault(obj => obj.Integer64ID == dataObject.Integer64ID)
+ ?? throw new IDNotFoundException(dataObject.Integer64ID.ToString());
- if (!AllowToUpdate(databaseDataObject, dataObject))
+ if (IsOldDataObjectDetectionEnabled && AllowToUpdate(databaseDataObject, dataObject) is false)
{
throw new DataObjectUpdateConflictException($"Failed to update because the data object was updated by {databaseDataObject.LastEditedBy ?? databaseDataObject.LastEditedByID ?? string.Empty} on {databaseDataObject.LastEditedOn}.");
}
@@ -137,7 +162,7 @@ public async override Task> UpdateAsync(List dataObjects, Cancellatio
}
}
- OnUpdated(new UpdatedEventArgs([.. returnDataObjects]));
+ OnUpdated(new UpdatedEventArgs([..returnDataObjects]));
return await Task.FromResult(returnDataObjects);
}
@@ -149,7 +174,7 @@ public async override Task> ValidateAsync(T dataObject, C
if (await ExistAsync(obj => obj.Integer64ID != dataObject.Integer64ID && obj.Name == dataObject.Name, cancellationToken) == true)
{
- validationResults.Add(new ValidationResult($"The {dataObject.Name} name already exists in the data store.", new List() { nameof(dataObject.Name) }));
+ validationResults.Add(new ValidationResult($"The {dataObject.Name} name already exists in the data store.", [nameof(dataObject.Name)]));
}
return validationResults;
From 0d1557c66028f021cafbddac42470bc229307c77 Mon Sep 17 00:00:00 2001
From: jmayer913 <72579603+jmayer913@users.noreply.github.com>
Date: Thu, 18 Sep 2025 10:06:12 -0400
Subject: [PATCH 3/4] Added unit tests for the new properties
---
.../DataLayer/IUserEditableDataLayer.cs | 6 +-
.../Database/UserEditableDataLayerUnitTest.cs | 57 +++++++++++++++++++
2 files changed, 60 insertions(+), 3 deletions(-)
diff --git a/JMayer.Data/Database/DataLayer/IUserEditableDataLayer.cs b/JMayer.Data/Database/DataLayer/IUserEditableDataLayer.cs
index 2997971..6df330a 100644
--- a/JMayer.Data/Database/DataLayer/IUserEditableDataLayer.cs
+++ b/JMayer.Data/Database/DataLayer/IUserEditableDataLayer.cs
@@ -11,15 +11,15 @@ namespace JMayer.Data.Database.DataLayer;
public interface IUserEditableDataLayer : IStandardCRUDDataLayer where T : UserEditableDataObject
{
///
- /// The property gets/sets if milliseconds, microseconds and nanoseconds are ignored when comparing the LastEditedOn
- /// for old data object detection.
+ /// The property gets/sets if milliseconds, microseconds and nanoseconds are ignored when comparing the
+ /// UserEditableDataObject.LastEditedOn property for old data object detection.
///
///
/// Syncfusion converts all DateTime properties in an object to UTC and the millseconds, microseconds and nanoseconds
/// are stipped from the DateTime. Because of this, the data layer would always detect old data and throw a DataObjectUpdateConflictException
/// so this property was introduced when the comparison needs to be less precise.
///
- /// This property is ignored if the IsOldDataObjectDetectionEnabled set to false.
+ /// This property is ignored if the IsOldDataObjectDetectionEnabled property is set to false.
///
bool IsLessPreciseTimestampComparisonEnabled { get; set; }
diff --git a/TestProject/Test/Database/UserEditableDataLayerUnitTest.cs b/TestProject/Test/Database/UserEditableDataLayerUnitTest.cs
index e9cf4f6..c7b6e37 100644
--- a/TestProject/Test/Database/UserEditableDataLayerUnitTest.cs
+++ b/TestProject/Test/Database/UserEditableDataLayerUnitTest.cs
@@ -254,6 +254,61 @@ public async Task VerifyGetPageListView()
[Fact]
public async Task VerifyGetPageListViewThrowsArgumentNullException() => await Assert.ThrowsAsync(() => new SimpleUserEditableDataLayer().GetPageListViewAsync(null));
+ ///
+ /// The method verifies when IsOldDataObjectDetectionEnabled is false no exception is thrown.
+ ///
+ /// A Task for the async.
+ ///
+ /// This is relying on when IsOldDataObjectDetectionEnabled is true and there's a conflict, an exception
+ /// is thrown. Xunit fails the test when an unhandle exception occurs so this passes when no exception is thrown.
+ ///
+ [Fact]
+ public async Task VerifyUpdateDisableOldDataObjectDetectionThrowsNoException()
+ {
+ SimpleUserEditableDataLayer dataLayer = new()
+ {
+ IsOldDataObjectDetectionEnabled = false,
+ };
+ SimpleUserEditableDataObject originalDataObject = await dataLayer.CreateAsync(new SimpleUserEditableDataObject() { Name = DefaultName });
+
+ _ = await dataLayer.UpdateAsync(new SimpleUserEditableDataObject(originalDataObject)
+ {
+ Value = DefaultValue,
+ });
+
+ originalDataObject.Value = DefaultValue + 1;
+ _ = await dataLayer.UpdateAsync(originalDataObject);
+ }
+
+ ///
+ /// The method verifies when IsLessPreciseTimestampComparisonEnabled is true and LastEditedOn only has a time difference in the milliseconds or below no exception is thrown.
+ ///
+ /// A Task for the async.
+ ///
+ /// This is relying on when a conflict is detected, an exception is thrown. Xunit fails the test
+ /// when an unhandle exception occurs so this passes when no exception is thrown.
+ ///
+ [Fact]
+ public async Task VerifyUpdateEnableLessPreciseTimestampComparisonThrowsNoException()
+ {
+#warning For the VerifyUpdateEnableLessPreciseTimestampComparisonThrowsNoException() test, I set LastEditedOn property to DateTime.Now without milliseconds. Create doesn't clear the LastEditedOn property but maybe it should because the object has just been added to the memory storage and the LastEditedOn property should be null.
+
+ SimpleUserEditableDataLayer dataLayer = new()
+ {
+ IsLessPreciseTimestampComparisonEnabled = true,
+ };
+ SimpleUserEditableDataObject originalDataObject = await dataLayer.CreateAsync(new SimpleUserEditableDataObject() { Name = DefaultName, LastEditedOn = new DateTime(DateTime.Now.Year, DateTime.Now.Month, DateTime.Now.Day, DateTime.Now.Hour, DateTime.Now.Minute, DateTime.Now.Second) });
+
+ originalDataObject.Value = DefaultValue + 1;
+
+ if (originalDataObject.LastEditedOn is not null)
+ {
+ originalDataObject.LastEditedOn = originalDataObject.LastEditedOn.Value.AddMilliseconds(1);
+ }
+
+ _ = await dataLayer.UpdateAsync(originalDataObject);
+ }
+
///
/// The method verifies the UserEditableMemoryDataLayer.Updated event fires when UserEditableMemoryDataLayer.UpdateAsync() is called.
///
@@ -367,6 +422,8 @@ await Assert.ThrowsAsync(async Task () =>
});
}
+
+
///
/// The method verifies UserEditableMemoryDataLayer.UpdateAsync() updates a list of data objects in the data store.
///
From e7b9ea265e353497df731750480b943c28f998b9 Mon Sep 17 00:00:00 2001
From: jmayer913 <72579603+jmayer913@users.noreply.github.com>
Date: Sun, 21 Sep 2025 09:53:03 -0400
Subject: [PATCH 4/4] Switched from set to init
---
.../DataLayer/IUserEditableDataLayer.cs | 22 ++++++++++++++-----
.../MemoryStorage/UserEditableDataLayer.cs | 4 ++--
2 files changed, 18 insertions(+), 8 deletions(-)
diff --git a/JMayer.Data/Database/DataLayer/IUserEditableDataLayer.cs b/JMayer.Data/Database/DataLayer/IUserEditableDataLayer.cs
index 6df330a..47b4cac 100644
--- a/JMayer.Data/Database/DataLayer/IUserEditableDataLayer.cs
+++ b/JMayer.Data/Database/DataLayer/IUserEditableDataLayer.cs
@@ -15,13 +15,19 @@ public interface IUserEditableDataLayer : IStandardCRUDDataLayer where T :
/// UserEditableDataObject.LastEditedOn property for old data object detection.
///
///
- /// Syncfusion converts all DateTime properties in an object to UTC and the millseconds, microseconds and nanoseconds
- /// are stipped from the DateTime. Because of this, the data layer would always detect old data and throw a DataObjectUpdateConflictException
- /// so this property was introduced when the comparison needs to be less precise.
- ///
+ /// With client/server communication, its possible the milliseconds, microseconds and nanoseconds are stipped
+ /// from the DateTime when data is sent to the server. Because of this, the data layer would always detect old
+ /// data and throw a DataObjectUpdateConflictException so this property was introduced when the comparison needs
+ /// to be less precise.
+ ///
+ ///
+ /// The default functionality is strong precision but when you need less precision then in the constructor of your
+ /// child class, set this property to true.
+ ///
+ ///
/// This property is ignored if the IsOldDataObjectDetectionEnabled property is set to false.
///
- bool IsLessPreciseTimestampComparisonEnabled { get; set; }
+ bool IsLessPreciseTimestampComparisonEnabled { get; init; }
///
/// The property gets/sets if the data layer checks if the data object being updated is considered old.
@@ -32,8 +38,12 @@ public interface IUserEditableDataLayer : IStandardCRUDDataLayer where T :
/// timestamps are the same then no one else has edited the data object and the update will occur. If the
/// timestamps are not the same, another user has edited the data object and the data layer throws a
/// DataObjectUpdateConflictException.
+ ///
+ ///
+ /// The default functionality is to check for data submission conflicts between users but when you don't want this
+ /// then in the constructor of your child class, set this property to false.
///
- bool IsOldDataObjectDetectionEnabled { get; set; }
+ bool IsOldDataObjectDetectionEnabled { get; init; }
///
/// The method returns all the data objects for the collection/table as a list view based on a where predicate with an order.
diff --git a/JMayer.Data/Database/DataLayer/MemoryStorage/UserEditableDataLayer.cs b/JMayer.Data/Database/DataLayer/MemoryStorage/UserEditableDataLayer.cs
index 2a3e31b..793fdbe 100644
--- a/JMayer.Data/Database/DataLayer/MemoryStorage/UserEditableDataLayer.cs
+++ b/JMayer.Data/Database/DataLayer/MemoryStorage/UserEditableDataLayer.cs
@@ -24,10 +24,10 @@ namespace JMayer.Data.Database.DataLayer.MemoryStorage;
public class UserEditableDataLayer : StandardCRUDDataLayer, IUserEditableDataLayer where T : UserEditableDataObject, new()
{
///
- public bool IsLessPreciseTimestampComparisonEnabled { get; set; }
+ public bool IsLessPreciseTimestampComparisonEnabled { get; init; }
///
- public bool IsOldDataObjectDetectionEnabled { get; set; } = true;
+ public bool IsOldDataObjectDetectionEnabled { get; init; } = true;
///
/// The method returns if there's no update conflict.