Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions JMayer.Data/Database/DataLayer/IUserEditableDataLayer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,41 @@ namespace JMayer.Data.Database.DataLayer;
/// <typeparam name="T">A UserEditableDataObject which represents the user editable data in the collection/table.</typeparam>
public interface IUserEditableDataLayer<T> : IStandardCRUDDataLayer<T> where T : UserEditableDataObject
{
/// <summary>
/// The property gets/sets if milliseconds, microseconds and nanoseconds are ignored when comparing the
/// UserEditableDataObject.LastEditedOn property for old data object detection.
/// </summary>
/// <remarks>
/// 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.
/// <br/>
/// <br/>
/// 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.
/// <br/>
/// <br/>
/// This property is ignored if the IsOldDataObjectDetectionEnabled property is set to false.
/// </remarks>
bool IsLessPreciseTimestampComparisonEnabled { get; init; }

/// <summary>
/// The property gets/sets if the data layer checks if the data object being updated is considered old.
/// </summary>
/// <remarks>
/// 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.
/// <br/>
/// <br/>
/// 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.
/// </remarks>
bool IsOldDataObjectDetectionEnabled { get; init; }

/// <summary>
/// The method returns all the data objects for the collection/table as a list view based on a where predicate with an order.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
/// </para>
/// <para>
/// 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.
/// </para>
/// </remarks>
public class UserEditableDataLayer<T> : StandardCRUDDataLayer<T>, IUserEditableDataLayer<T> where T : UserEditableDataObject, new()
{
/// <inheritdoc/>
public bool IsLessPreciseTimestampComparisonEnabled { get; init; }

/// <inheritdoc/>
public bool IsOldDataObjectDetectionEnabled { get; init; } = true;

/// <summary>
/// The method returns if there's no update conflict.
/// </summary>
/// <param name="databaseDataObject">The data object in the database.</param>
/// <param name="userDataObject">The data object updated by the user.</param>
/// <returns>True means the data object can be updated; false means there's a conflict.</returns>
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;
}
}

/// <summary>
Expand Down Expand Up @@ -112,14 +141,10 @@ public async override Task<List<T>> UpdateAsync(List<T> 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}.");
}
Expand All @@ -137,7 +162,7 @@ public async override Task<List<T>> UpdateAsync(List<T> dataObjects, Cancellatio
}
}

OnUpdated(new UpdatedEventArgs([.. returnDataObjects]));
OnUpdated(new UpdatedEventArgs([..returnDataObjects]));

return await Task.FromResult(returnDataObjects);
}
Expand All @@ -149,7 +174,7 @@ public async override Task<List<ValidationResult>> 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<string>() { nameof(dataObject.Name) }));
validationResults.Add(new ValidationResult($"The {dataObject.Name} name already exists in the data store.", [nameof(dataObject.Name)]));
}

return validationResults;
Expand Down
2 changes: 1 addition & 1 deletion JMayer.Data/JMayer.Data.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<Version>1.2.2</Version>
<Version>1.3.0</Version>
<Authors>jmayer913</Authors>
<Company>jmayer913</Company>
<PackageDescription>Package contains generic data access via HTTP or a Database.</PackageDescription>
Expand Down
57 changes: 57 additions & 0 deletions TestProject/Test/Database/UserEditableDataLayerUnitTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@
});
PagedList<ListView> sortPage = await dataLayer.GetPageListViewAsync(sortQueryDefinition);

#warning This should be broken into multiple tests because its trying to test too many things.

Check warning on line 233 in TestProject/Test/Database/UserEditableDataLayerUnitTest.cs

View workflow job for this annotation

GitHub Actions / build

#warning: 'This should be broken into multiple tests because its trying to test too many things.'

//Verify the skip and take.
Assert.Equal(100, skipAndTakePage.TotalRecords);
Expand All @@ -254,6 +254,61 @@
[Fact]
public async Task VerifyGetPageListViewThrowsArgumentNullException() => await Assert.ThrowsAsync<ArgumentNullException>(() => new SimpleUserEditableDataLayer().GetPageListViewAsync(null));

/// <summary>
/// The method verifies when IsOldDataObjectDetectionEnabled is false no exception is thrown.
/// </summary>
/// <returns>A Task for the async.</returns>
/// <remarks>
/// 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.
/// </remarks>
[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);
}

/// <summary>
/// The method verifies when IsLessPreciseTimestampComparisonEnabled is true and LastEditedOn only has a time difference in the milliseconds or below no exception is thrown.
/// </summary>
/// <returns>A Task for the async.</returns>
/// <remarks>
/// 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.
/// </remarks>
[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.

Check warning on line 294 in TestProject/Test/Database/UserEditableDataLayerUnitTest.cs

View workflow job for this annotation

GitHub Actions / build

#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);
}

/// <summary>
/// The method verifies the UserEditableMemoryDataLayer.Updated event fires when UserEditableMemoryDataLayer.UpdateAsync() is called.
/// </summary>
Expand Down Expand Up @@ -367,6 +422,8 @@
});
}



/// <summary>
/// The method verifies UserEditableMemoryDataLayer.UpdateAsync() updates a list of data objects in the data store.
/// </summary>
Expand Down
Loading