diff --git a/Sources/Linq2DynamoDb.DataContext.Tests/Entities/Book.cs b/Sources/Linq2DynamoDb.DataContext.Tests/Entities/Book.cs index 37aeb9d..63a6a34 100644 --- a/Sources/Linq2DynamoDb.DataContext.Tests/Entities/Book.cs +++ b/Sources/Linq2DynamoDb.DataContext.Tests/Entities/Book.cs @@ -32,6 +32,9 @@ public class Book : EntityBase [DynamoDBProperty(typeof(StringTimeSpanDictionaryConverter))] public IDictionary FilmsBasedOnBook { get; set; } + [DynamoDBVersion] + public int? VersionNumber { get; set; } + public enum Popularity { Low, diff --git a/Sources/Linq2DynamoDb.DataContext.Tests/Entities/BookPoco.cs b/Sources/Linq2DynamoDb.DataContext.Tests/Entities/BookPoco.cs index 6db08f8..f5fa232 100644 --- a/Sources/Linq2DynamoDb.DataContext.Tests/Entities/BookPoco.cs +++ b/Sources/Linq2DynamoDb.DataContext.Tests/Entities/BookPoco.cs @@ -30,6 +30,9 @@ public class BookPoco { [DynamoDBProperty(typeof(StringTimeSpanDictionaryConverter))] public IDictionary FilmsBasedOnBook { get; set; } + [DynamoDBVersion] + public int? VersionNumber { get; set; } + public enum Popularity { Low, diff --git a/Sources/Linq2DynamoDb.DataContext.Tests/EntityManagementTests/EntityCreationTests.cs b/Sources/Linq2DynamoDb.DataContext.Tests/EntityManagementTests/EntityCreationTests.cs index e57aaf0..67514f2 100644 --- a/Sources/Linq2DynamoDb.DataContext.Tests/EntityManagementTests/EntityCreationTests.cs +++ b/Sources/Linq2DynamoDb.DataContext.Tests/EntityManagementTests/EntityCreationTests.cs @@ -33,17 +33,18 @@ public void DataContext_EntityCreation_PersistsRecordToDynamoDb() Assert.IsNotNull(storedBook); } - [Ignore("This behavior is currently expected. SubmitChanges() uses DocumentBatchWrite, which only supports PUT operations, which by default replaces existing entities")] [Test] - [ExpectedException(typeof(InvalidOperationException), ExpectedMessage = "cannot be added, because entity with that key already exists", MatchType = MessageMatch.Contains)] + [ExpectedException(typeof(AggregateException))] public void DataContext_EntityCreation_ThrowsExceptionWhenEntityAlreadyExistsInDynamoDbButWasNeverQueriedInCurrentContext() { - var book = BooksHelper.CreateBook(popularityRating: Book.Popularity.Average); + var persistedBook = BooksHelper.CreateBook(popularityRating: Book.Popularity.Average); - book.PopularityRating = Book.Popularity.High; + var bookCopy = BooksHelper.CreateBook(name: persistedBook.Name, publishYear: persistedBook.PublishYear, persistToDynamoDb: false); + + bookCopy.PopularityRating = Book.Popularity.High; var booksTable = this.Context.GetTable(); - booksTable.InsertOnSubmit(book); + booksTable.InsertOnSubmit(bookCopy); this.Context.SubmitChanges(); } diff --git a/Sources/Linq2DynamoDb.DataContext.Tests/EntityManagementTests/EntityModificationTests.cs b/Sources/Linq2DynamoDb.DataContext.Tests/EntityManagementTests/EntityModificationTests.cs index a36ffce..5887a1d 100644 --- a/Sources/Linq2DynamoDb.DataContext.Tests/EntityManagementTests/EntityModificationTests.cs +++ b/Sources/Linq2DynamoDb.DataContext.Tests/EntityManagementTests/EntityModificationTests.cs @@ -1,7 +1,9 @@ using Linq2DynamoDb.DataContext.Tests.Entities; using Linq2DynamoDb.DataContext.Tests.Helpers; using NUnit.Framework; +using System; using System.Collections.Generic; +using System.Linq; namespace Linq2DynamoDb.DataContext.Tests.EntityManagementTests { @@ -50,28 +52,7 @@ public void DataContext_EntityModification_UpdateRecordWithNewArray() CollectionAssert.AreEquivalent(storedBook.RentingHistory, storedBookAfterModification.RentingHistory); } - - [Ignore("This behavior is currently expected. SubmitChanges() uses DocumentBatchWrite, which only supports PUT operations with default 'replace' behavior")] - [Test] - public void DataContext_EntityModification_UpdateShouldNotAffectFieldsModifiedFromOutside() - { - var book = BooksHelper.CreateBook(popularityRating: Book.Popularity.Average, persistToDynamoDb: false); - - var booksTable = this.Context.GetTable(); - booksTable.InsertOnSubmit(book); - this.Context.SubmitChanges(); - - // Update record from outside of DataTable - BooksHelper.CreateBook(book.Name, book.PublishYear, numPages: 15); - - book.PopularityRating = Book.Popularity.High; - this.Context.SubmitChanges(); - - var storedBook = booksTable.Find(book.Name, book.PublishYear); - Assert.AreEqual(book.PopularityRating, storedBook.PopularityRating, "Record was not updated"); - Assert.AreEqual(book.NumPages, 15, "Update has erased changes from outside"); - } - + [Test] public void DataContext_UpdateEntity_UpdatesRecordWhenOldRecordIsNull() { diff --git a/Sources/Linq2DynamoDb.DataContext.Tests/EntityManagementTests/Poco/PocoCreationTests.cs b/Sources/Linq2DynamoDb.DataContext.Tests/EntityManagementTests/Poco/PocoCreationTests.cs index de540a0..a9d6ed6 100644 --- a/Sources/Linq2DynamoDb.DataContext.Tests/EntityManagementTests/Poco/PocoCreationTests.cs +++ b/Sources/Linq2DynamoDb.DataContext.Tests/EntityManagementTests/Poco/PocoCreationTests.cs @@ -33,20 +33,6 @@ public void DataContext_EntityCreation_PersistsRecordToDynamoDb() Assert.IsNotNull(storedBookPoco); } - [Ignore("This behavior is currently expected. SubmitChanges() uses DocumentBatchWrite, which only supports PUT operations, which by default replaces existing entities")] - [Test] - [ExpectedException(typeof(InvalidOperationException), ExpectedMessage = "cannot be added, because entity with that key already exists", MatchType = MessageMatch.Contains)] - public void DataContext_EntityCreation_ThrowsExceptionWhenEntityAlreadyExistsInDynamoDbButWasNeverQueriedInCurrentContext() - { - var book = BookPocosHelper.CreateBookPoco(popularityRating: BookPoco.Popularity.Average); - - book.PopularityRating = BookPoco.Popularity.High; - - var booksTable = this.Context.GetTable(); - booksTable.InsertOnSubmit(book); - this.Context.SubmitChanges(); - } - [Test] [ExpectedException(typeof(InvalidOperationException), ExpectedMessage = "cannot be added, because entity with that key already exists", MatchType = MessageMatch.Contains)] public void DataContext_EntityCreation_ThrowsExceptionWhenTryingToAddSameEntityTwice() diff --git a/Sources/Linq2DynamoDb.DataContext.Tests/EntityManagementTests/Poco/PocoModificationTests.cs b/Sources/Linq2DynamoDb.DataContext.Tests/EntityManagementTests/Poco/PocoModificationTests.cs index d581bef..8a31bbd 100644 --- a/Sources/Linq2DynamoDb.DataContext.Tests/EntityManagementTests/Poco/PocoModificationTests.cs +++ b/Sources/Linq2DynamoDb.DataContext.Tests/EntityManagementTests/Poco/PocoModificationTests.cs @@ -51,27 +51,6 @@ public void DataContext_EntityModification_UpdateRecordWithNewArray() CollectionAssert.AreEquivalent(storedBookPoco.RentingHistory, storedBookPocoAfterModification.RentingHistory); } - [Ignore("This behavior is currently expected. SubmitChanges() uses DocumentBatchWrite, which only supports PUT operations with default 'replace' behavior")] - [Test] - public void DataContext_EntityModification_UpdateShouldNotAffectFieldsModifiedFromOutside() - { - var book = BookPocosHelper.CreateBookPoco(popularityRating: BookPoco.Popularity.Average, persistToDynamoDb: false); - - var booksTable = this.Context.GetTable(); - booksTable.InsertOnSubmit(book); - this.Context.SubmitChanges(); - - // Update record from outside of DataTable - BookPocosHelper.CreateBookPoco(book.Name, book.PublishYear, numPages: 15); - - book.PopularityRating = BookPoco.Popularity.High; - this.Context.SubmitChanges(); - - var storedBookPoco = booksTable.Find(book.Name, book.PublishYear); - Assert.AreEqual(book.PopularityRating, storedBookPoco.PopularityRating, "Record was not updated"); - Assert.AreEqual(book.NumPages, 15, "Update has erased changes from outside"); - } - [Test] public void DataContext_UpdateEntity_UpdatesRecordWhenOldRecordIsNull() { diff --git a/Sources/Linq2DynamoDb.DataContext.Tests/EntityManagementTests/Versioning/EntityVersioningTests.cs b/Sources/Linq2DynamoDb.DataContext.Tests/EntityManagementTests/Versioning/EntityVersioningTests.cs new file mode 100644 index 0000000..2405119 --- /dev/null +++ b/Sources/Linq2DynamoDb.DataContext.Tests/EntityManagementTests/Versioning/EntityVersioningTests.cs @@ -0,0 +1,91 @@ +using Linq2DynamoDb.DataContext.Tests.Entities; +using Linq2DynamoDb.DataContext.Tests.Helpers; +using NUnit.Framework; +using System; +using System.Collections.Generic; + +namespace Linq2DynamoDb.DataContext.Tests.EntityManagementTests.Versioning +{ + [TestFixture] + class EntityVersioningTests : DataContextTestBase + { + public override void SetUp() + { + } + + public override void TearDown() + { + } + + [Test] + [ExpectedException(typeof(AggregateException))] + public void DataContext_UpdateEntity_Does_OptimisticLocking() + { + var contextA = TestConfiguration.GetDataContext(); + var contextB = TestConfiguration.GetDataContext(); + + var originalBook = BooksHelper.CreateBook(popularityRating: Book.Popularity.Low, rentingHistory: null); + var booksTableA = contextA.GetTable(); + var booksTableB = contextB.GetTable(); + + // Read the same entry from the database into two contexts + var retrievedBookA = booksTableA.Find(originalBook.Name, originalBook.PublishYear); + var retrievedBookB = booksTableB.Find(originalBook.Name, originalBook.PublishYear); + + // Mutate a property on instance A and persist + retrievedBookA.PopularityRating = Book.Popularity.Average; + contextA.SubmitChanges(); + + // Mutate a property on instance B (unaware of changes to A) + retrievedBookB.RentingHistory = new List { "history element" }; + contextB.SubmitChanges(); + } + + [Test] + [ExpectedException(typeof(AggregateException))] + public void DataContext_AddEntity_DoesNotOverwrite_ExistingVersionedEntity() + { + var contextA = TestConfiguration.GetDataContext(); + var contextB = TestConfiguration.GetDataContext(); + + var tableA = contextA.GetTable(); + var tableB = contextB.GetTable(); + + var bookA = BooksHelper.CreateBook(name: "A Tale of Two Books", publishYear: 0, persistToDynamoDb: false); + var bookB = BooksHelper.CreateBook(name: "A Tale of Two Books", publishYear: 0, persistToDynamoDb: false); + + + tableA.InsertOnSubmit(bookA); + contextA.SubmitChanges(); + + tableB.InsertOnSubmit(bookB); + contextB.SubmitChanges(); + } + + [Test] + [ExpectedException(typeof(AggregateException))] + public void DataContext_RemoveEntity_RespectsVersionConstraint() + { + var book = BooksHelper.CreateBook(numPages: 5, persistToDynamoDb: false); + + var contextA = TestConfiguration.GetDataContext(); + var contextB = TestConfiguration.GetDataContext(); + + var tableA = contextA.GetTable(); + var tableB = contextB.GetTable(); + + // Insert the book in Context A + tableA.InsertOnSubmit(book); + contextA.SubmitChanges(); + + // Find and modify the book in Context B + var retrievedBook = tableB.Find(book.Name, book.PublishYear); + retrievedBook.NumPages = 10; + contextB.SubmitChanges(); + + // Try to delete the book from Context A + tableA.RemoveOnSubmit(book); + contextA.SubmitChanges(); + } + } +} diff --git a/Sources/Linq2DynamoDb.DataContext.Tests/Helpers/BooksHelper.cs b/Sources/Linq2DynamoDb.DataContext.Tests/Helpers/BooksHelper.cs index a05311a..d51be37 100644 --- a/Sources/Linq2DynamoDb.DataContext.Tests/Helpers/BooksHelper.cs +++ b/Sources/Linq2DynamoDb.DataContext.Tests/Helpers/BooksHelper.cs @@ -28,7 +28,7 @@ public static void CleanSession() { Logger.DebugFormat("Removing {0} records from DynamoDb", _recordsForCleanup.Count); - Parallel.ForEach(_recordsForCleanup, book => PersistenceContext.Delete(book)); + Parallel.ForEach(_recordsForCleanup, book => PersistenceContext.Delete(book, new DynamoDBOperationConfig { SkipVersionCheck = true, ConsistentRead = true })); _recordsForCleanup = new ConcurrentQueue(); } diff --git a/Sources/Linq2DynamoDb.DataContext.Tests/Linq2DynamoDb.DataContext.Tests.csproj b/Sources/Linq2DynamoDb.DataContext.Tests/Linq2DynamoDb.DataContext.Tests.csproj index bdd4fbf..faa3946 100644 --- a/Sources/Linq2DynamoDb.DataContext.Tests/Linq2DynamoDb.DataContext.Tests.csproj +++ b/Sources/Linq2DynamoDb.DataContext.Tests/Linq2DynamoDb.DataContext.Tests.csproj @@ -90,6 +90,7 @@ + @@ -140,7 +141,9 @@ Designer - + + +