diff --git a/src/core/Wemogy.Infrastructure.Database.Core.UnitTests/DatabaseRepositories/IUserRepository.cs b/src/core/Wemogy.Infrastructure.Database.Core.UnitTests/DatabaseRepositories/IUserRepository.cs index 6b0a60d..bca534f 100644 --- a/src/core/Wemogy.Infrastructure.Database.Core.UnitTests/DatabaseRepositories/IUserRepository.cs +++ b/src/core/Wemogy.Infrastructure.Database.Core.UnitTests/DatabaseRepositories/IUserRepository.cs @@ -6,7 +6,7 @@ namespace Wemogy.Infrastructure.Database.Core.UnitTests.DatabaseRepositories; -[RepositoryOptions(enableSoftDelete: true)] +[RepositoryOptions(enableSoftDelete: false)] [RepositoryReadFilter(typeof(GeneralUserReadFilter))] [RepositoryPropertyFilter(typeof(GeneralUserPropertyFilter))] public interface IUserRepository : IDatabaseRepository diff --git a/src/core/Wemogy.Infrastructure.Database.Core.UnitTests/Plugins/MultiTenantDatabase/MultiTenantDatabaseRepositoryTestsBase.DeleteAsync.cs b/src/core/Wemogy.Infrastructure.Database.Core.UnitTests/Plugins/MultiTenantDatabase/MultiTenantDatabaseRepositoryTestsBase.DeleteAsync.cs index 5a5ab09..5e088ee 100644 --- a/src/core/Wemogy.Infrastructure.Database.Core.UnitTests/Plugins/MultiTenantDatabase/MultiTenantDatabaseRepositoryTestsBase.DeleteAsync.cs +++ b/src/core/Wemogy.Infrastructure.Database.Core.UnitTests/Plugins/MultiTenantDatabase/MultiTenantDatabaseRepositoryTestsBase.DeleteAsync.cs @@ -122,20 +122,19 @@ public async Task DeleteByPredicateForPartitionKeyAsync_ShouldDeleteFromCorrectT await MicrosoftUserRepository.CreateAsync(user2); // Act - await MicrosoftUserRepository.DeleteAsync(u => u.TenantId == user2.TenantId && u.Id == user2.Id); + var count = await MicrosoftUserRepository.DeleteAsync(u => u.TenantId == user2.TenantId && u.Id == user2.Id); // Assert var msEntities = await MicrosoftUserRepository.GetAllAsync(); msEntities.Should().HaveCount(1); msEntities.Should().ContainSingle(u => u.Id == user1.Id); + count.Should().Be(1); } [Fact] - public async Task DeleteShouldThrowIfNotExists() + public async Task DeleteByIdAndPartitionKeyShouldThrowIfNotExists() { - // TODO: This is inconsistent to the other DeleteAsync behaviours -> DeleteAsync(id) does not throw. - // Arrange await ResetAsync(); @@ -149,4 +148,43 @@ public async Task DeleteShouldThrowIfNotExists() exception1.Should().BeOfType(); AssertExceptionMessageDoesNotContainPrefix(exception1); } + + [Fact] + public async Task DeleteByIdShouldThrowIfNotExists() + { + // Arrange + await ResetAsync(); + + // Act + var exception1 = await Record.ExceptionAsync( + () => MicrosoftUserRepository.DeleteAsync("123")); + + // Assert + exception1.Should().BeOfType(); + AssertExceptionMessageDoesNotContainPrefix(exception1); + } + + [Fact] + public async Task MultitenantDeleteAllAsync_ShouldWork() + { + // Arrange + await ResetAsync(); + await AppleUserRepository.CreateAsync(User.Faker.Generate()); + await AppleUserRepository.CreateAsync(User.Faker.Generate()); + await MicrosoftUserRepository.CreateAsync(User.Faker.Generate()); + await MicrosoftUserRepository.CreateAsync(User.Faker.Generate()); + await MicrosoftUserRepository.CreateAsync(User.Faker.Generate()); + await MicrosoftUserRepository.CreateAsync(User.Faker.Generate()); + + // Act + var count = await MicrosoftUserRepository.DeleteAsync(x => true); + + // Assert + var entities = await MicrosoftUserRepository.QueryAsync(x => true); + entities.Should().BeEmpty(); + count.Should().Be(4); + + var appleEntities = await AppleUserRepository.QueryAsync(x => true); + appleEntities.Count.Should().Be(2); + } } diff --git a/src/core/Wemogy.Infrastructure.Database.Core.UnitTests/Plugins/MultiTenantDatabase/MultiTenantDatabaseRepositoryTestsBase.EnsureExistsAsync.cs b/src/core/Wemogy.Infrastructure.Database.Core.UnitTests/Plugins/MultiTenantDatabase/MultiTenantDatabaseRepositoryTestsBase.EnsureExistsAsync.cs index 7033a56..3aabe40 100644 --- a/src/core/Wemogy.Infrastructure.Database.Core.UnitTests/Plugins/MultiTenantDatabase/MultiTenantDatabaseRepositoryTestsBase.EnsureExistsAsync.cs +++ b/src/core/Wemogy.Infrastructure.Database.Core.UnitTests/Plugins/MultiTenantDatabase/MultiTenantDatabaseRepositoryTestsBase.EnsureExistsAsync.cs @@ -69,7 +69,7 @@ public async Task EnsureExistsByPredicateForPartitionKeyMultipleAsync_ShouldWork var user = User.Faker.Generate(); await MicrosoftUserRepository.CreateAsync(user); - // Act - TODO: Predicate does not support the PartitionKey + // Act var exception = await Record.ExceptionAsync( () => MicrosoftUserRepository.EnsureExistsAsync(u => u.TenantId == user.TenantId)); diff --git a/src/core/Wemogy.Infrastructure.Database.Core.UnitTests/Plugins/MultiTenantDatabase/MultiTenantDatabaseRepositoryTestsBase.GetAsync.cs b/src/core/Wemogy.Infrastructure.Database.Core.UnitTests/Plugins/MultiTenantDatabase/MultiTenantDatabaseRepositoryTestsBase.GetAsync.cs index 30207c0..09387ef 100644 --- a/src/core/Wemogy.Infrastructure.Database.Core.UnitTests/Plugins/MultiTenantDatabase/MultiTenantDatabaseRepositoryTestsBase.GetAsync.cs +++ b/src/core/Wemogy.Infrastructure.Database.Core.UnitTests/Plugins/MultiTenantDatabase/MultiTenantDatabaseRepositoryTestsBase.GetAsync.cs @@ -75,56 +75,6 @@ public async Task GetAsyncMultiple_ShouldGetExistingItemsById() AssertExceptionMessageDoesNotContainPrefix(exception2); } - [Fact] - public async Task GetAsyncMultiple_ShouldGetExistingItemsByPredicate() - { - // Arrange - await ResetAsync(); - var user1 = User.Faker.Generate(); - var user2 = User.Faker.Generate(); - await MicrosoftUserRepository.CreateAsync(user1); - await AppleUserRepository.CreateAsync(user2); - - // Act - var msUserFromDb = await MicrosoftUserRepository.GetAsync(u => u.Firstname == user1.Firstname); - var appleUserFromDb = await AppleUserRepository.GetAsync(u => u.Lastname == user2.Lastname); - - // Assert - msUserFromDb.Should().BeEquivalentTo(user1); - appleUserFromDb.Should().BeEquivalentTo(user2); - AssertPartitionKeyPrefixIsRemoved(msUserFromDb); - AssertPartitionKeyPrefixIsRemoved(appleUserFromDb); - - var exception1 = await Record.ExceptionAsync(() => MicrosoftUserRepository.GetAsync(user2.Id)); - exception1.Should().BeOfType(); - AssertExceptionMessageDoesNotContainPrefix(exception1); - - var exception2 = await Record.ExceptionAsync(() => AppleUserRepository.GetAsync(user1.Id)); - exception2.Should().BeOfType(); - AssertExceptionMessageDoesNotContainPrefix(exception2); - } - - [Fact] - public async Task GetAsyncMultiple_ShouldGetExistingItemsByPredicateForPartitionKey() - { - // Arrange - await ResetAsync(); - var user1 = User.Faker.Generate(); - var user2 = User.Faker.Generate(); - await MicrosoftUserRepository.CreateAsync(user1); - await AppleUserRepository.CreateAsync(user2); - - // Act - var msUserFromDb = await MicrosoftUserRepository.GetAsync(u => u.TenantId == user1.TenantId); - var appleUserFromDb = await AppleUserRepository.GetAsync(u => u.TenantId == user2.TenantId && u.Id == user2.Id); - AssertPartitionKeyPrefixIsRemoved(msUserFromDb); - AssertPartitionKeyPrefixIsRemoved(appleUserFromDb); - - // Assert - msUserFromDb.Should().BeEquivalentTo(user1); - appleUserFromDb.Should().BeEquivalentTo(user2); - } - [Fact] public async Task GetAsyncMultiple_ShouldGetExistingItemsByIdAndPartitionKeyForSamePartitionKey() { @@ -198,60 +148,4 @@ public async Task GetAsyncMultiple_ShouldGetExistingItemsByIdForSamePartitionKey exception2.Should().BeOfType(); AssertExceptionMessageDoesNotContainPrefix(exception2); } - - [Fact] - public async Task GetAsyncMultiple_ShouldGetExistingItemsByPredicateForSamePartitionKey() - { - // Arrange - await ResetAsync(); - var user1 = User.Faker.Generate(); - user1.Firstname = "MS"; - var user2 = User.Faker.Generate(); - user2.TenantId = user1.TenantId; // fake same tenantId - user2.Firstname = "APPLE"; - await MicrosoftUserRepository.CreateAsync(user1); - await AppleUserRepository.CreateAsync(user2); - - // Act - var msUserFromDb = await MicrosoftUserRepository.GetAsync(u => u.Firstname == user1.Firstname); - var appleUserFromDb = await AppleUserRepository.GetAsync(u => u.Lastname == user2.Lastname); - - // Assert - msUserFromDb.Should().BeEquivalentTo(user1); - appleUserFromDb.Should().BeEquivalentTo(user2); - AssertPartitionKeyPrefixIsRemoved(msUserFromDb); - AssertPartitionKeyPrefixIsRemoved(appleUserFromDb); - - var exception1 = await Record.ExceptionAsync(() => MicrosoftUserRepository.GetAsync(user2.Id)); - exception1.Should().BeOfType(); - AssertExceptionMessageDoesNotContainPrefix(exception1); - - var exception2 = await Record.ExceptionAsync(() => AppleUserRepository.GetAsync(user1.Id)); - exception2.Should().BeOfType(); - AssertExceptionMessageDoesNotContainPrefix(exception2); - } - - [Fact] - public async Task GetAsyncMultiple_ShouldGetExistingItemsByPredicateWithPartitionKeyFilterForSamePartitionKey() - { - // Arrange - await ResetAsync(); - var user1 = User.Faker.Generate(); - user1.Firstname = "MS"; - var user2 = User.Faker.Generate(); - user2.TenantId = user1.TenantId; // fake same tenantId - user2.Firstname = "APPLE"; - await MicrosoftUserRepository.CreateAsync(user1); - await AppleUserRepository.CreateAsync(user2); - - // Act - TODO: PartitionKey not supported - var msUserFromDb = await MicrosoftUserRepository.GetAsync(u => u.TenantId == user1.TenantId); - var appleUserFromDb = await AppleUserRepository.GetAsync(u => u.TenantId == user2.TenantId && u.Id == user2.Id); - AssertPartitionKeyPrefixIsRemoved(msUserFromDb); - AssertPartitionKeyPrefixIsRemoved(appleUserFromDb); - - // Assert - msUserFromDb.Should().BeEquivalentTo(user1); - appleUserFromDb.Should().BeEquivalentTo(user2); - } } diff --git a/src/core/Wemogy.Infrastructure.Database.Core.UnitTests/Plugins/MultiTenantDatabase/MultiTenantDatabaseRepositoryTestsBase.QuerySingleAsync.cs b/src/core/Wemogy.Infrastructure.Database.Core.UnitTests/Plugins/MultiTenantDatabase/MultiTenantDatabaseRepositoryTestsBase.QuerySingleAsync.cs new file mode 100644 index 0000000..70ced39 --- /dev/null +++ b/src/core/Wemogy.Infrastructure.Database.Core.UnitTests/Plugins/MultiTenantDatabase/MultiTenantDatabaseRepositoryTestsBase.QuerySingleAsync.cs @@ -0,0 +1,118 @@ +using System.Threading.Tasks; +using FluentAssertions; +using Wemogy.Core.Errors.Exceptions; +using Wemogy.Infrastructure.Database.Core.UnitTests.Fakes.Entities; +using Xunit; + +namespace Wemogy.Infrastructure.Database.Core.UnitTests.Plugins.MultiTenantDatabase; + +public partial class MultiTenantDatabaseRepositoryTestsBase +{ + [Fact] + public async Task QuerySingleAsync_ShouldGetExistingItemsByPredicate() + { + // Arrange + await ResetAsync(); + var user1 = User.Faker.Generate(); + var user2 = User.Faker.Generate(); + await MicrosoftUserRepository.CreateAsync(user1); + await AppleUserRepository.CreateAsync(user2); + + // Act + var msUserFromDb = await MicrosoftUserRepository.QuerySingleAsync(u => u.Firstname == user1.Firstname); + var appleUserFromDb = await AppleUserRepository.QuerySingleAsync(u => u.Lastname == user2.Lastname); + + // Assert + msUserFromDb.Should().BeEquivalentTo(user1); + appleUserFromDb.Should().BeEquivalentTo(user2); + AssertPartitionKeyPrefixIsRemoved(msUserFromDb); + AssertPartitionKeyPrefixIsRemoved(appleUserFromDb); + + var exception1 = await Record.ExceptionAsync(() => MicrosoftUserRepository.GetAsync(user2.Id)); + exception1.Should().BeOfType(); + AssertExceptionMessageDoesNotContainPrefix(exception1); + + var exception2 = await Record.ExceptionAsync(() => AppleUserRepository.GetAsync(user1.Id)); + exception2.Should().BeOfType(); + AssertExceptionMessageDoesNotContainPrefix(exception2); + } + + [Fact] + public async Task QuerySingleAsync_ShouldGetExistingItemsByPredicateForPartitionKey() + { + // Arrange + await ResetAsync(); + var user1 = User.Faker.Generate(); + var user2 = User.Faker.Generate(); + await MicrosoftUserRepository.CreateAsync(user1); + await AppleUserRepository.CreateAsync(user2); + + // Act + var msUserFromDb = await MicrosoftUserRepository.QuerySingleAsync(u => u.TenantId == user1.TenantId); + var appleUserFromDb = + await AppleUserRepository.QuerySingleAsync(u => u.TenantId == user2.TenantId && u.Id == user2.Id); + AssertPartitionKeyPrefixIsRemoved(msUserFromDb); + AssertPartitionKeyPrefixIsRemoved(appleUserFromDb); + + // Assert + msUserFromDb.Should().BeEquivalentTo(user1); + appleUserFromDb.Should().BeEquivalentTo(user2); + } + + [Fact] + public async Task QuerySingleAsync_ShouldGetExistingItemsByPredicateForSamePartitionKey() + { + // Arrange + await ResetAsync(); + var user1 = User.Faker.Generate(); + user1.Firstname = "MS"; + var user2 = User.Faker.Generate(); + user2.TenantId = user1.TenantId; // fake same tenantId + user2.Firstname = "APPLE"; + await MicrosoftUserRepository.CreateAsync(user1); + await AppleUserRepository.CreateAsync(user2); + + // Act + var msUserFromDb = await MicrosoftUserRepository.QuerySingleAsync(u => u.Firstname == user1.Firstname); + var appleUserFromDb = await AppleUserRepository.QuerySingleAsync(u => u.Lastname == user2.Lastname); + + // Assert + msUserFromDb.Should().BeEquivalentTo(user1); + appleUserFromDb.Should().BeEquivalentTo(user2); + AssertPartitionKeyPrefixIsRemoved(msUserFromDb); + AssertPartitionKeyPrefixIsRemoved(appleUserFromDb); + + var exception1 = await Record.ExceptionAsync(() => MicrosoftUserRepository.GetAsync(user2.Id)); + exception1.Should().BeOfType(); + AssertExceptionMessageDoesNotContainPrefix(exception1); + + var exception2 = await Record.ExceptionAsync(() => AppleUserRepository.GetAsync(user1.Id)); + exception2.Should().BeOfType(); + AssertExceptionMessageDoesNotContainPrefix(exception2); + } + + [Fact] + public async Task QuerySingleAsync_ShouldGetExistingItemsByPredicateWithPartitionKeyFilterForSamePartitionKey() + { + // Arrange + await ResetAsync(); + var user1 = User.Faker.Generate(); + user1.Firstname = "MS"; + var user2 = User.Faker.Generate(); + user2.TenantId = user1.TenantId; // fake same tenantId + user2.Firstname = "APPLE"; + await MicrosoftUserRepository.CreateAsync(user1); + await AppleUserRepository.CreateAsync(user2); + + // Act - TODO: PartitionKey not supported + var msUserFromDb = await MicrosoftUserRepository.QuerySingleAsync(u => u.TenantId == user1.TenantId); + var appleUserFromDb = + await AppleUserRepository.QuerySingleAsync(u => u.TenantId == user2.TenantId && u.Id == user2.Id); + AssertPartitionKeyPrefixIsRemoved(msUserFromDb); + AssertPartitionKeyPrefixIsRemoved(appleUserFromDb); + + // Assert + msUserFromDb.Should().BeEquivalentTo(user1); + appleUserFromDb.Should().BeEquivalentTo(user2); + } +} diff --git a/src/core/Wemogy.Infrastructure.Database.Core.UnitTests/Plugins/MultiTenantDatabase/MultiTenantDatabaseRepositoryTestsBase.SoftDelete.cs b/src/core/Wemogy.Infrastructure.Database.Core.UnitTests/Plugins/MultiTenantDatabase/MultiTenantDatabaseRepositoryTestsBase.SoftDelete.cs new file mode 100644 index 0000000..f99bd69 --- /dev/null +++ b/src/core/Wemogy.Infrastructure.Database.Core.UnitTests/Plugins/MultiTenantDatabase/MultiTenantDatabaseRepositoryTestsBase.SoftDelete.cs @@ -0,0 +1,106 @@ +using System; +using System.Threading.Tasks; +using FluentAssertions; +using Wemogy.Core.Errors.Exceptions; +using Wemogy.Infrastructure.Database.Core.UnitTests.Fakes.Entities; +using Xunit; + +namespace Wemogy.Infrastructure.Database.Core.UnitTests.Plugins.MultiTenantDatabase; + +public partial class MultiTenantDatabaseRepositoryTestsBase +{ + [Fact] + public async Task GetAsync_ShouldThrow_ForSoftDeletedItemsForMultipleTenants() + { + await ResetAsync(); + MicrosoftUserRepository.SoftDeleteState.Enable(); + + // Arrange + var user = User.Faker.Generate(); + user.IsDeleted.Should().BeFalse(); + await MicrosoftUserRepository.CreateAsync(user); + await AppleUserRepository.CreateAsync(user); + + // Act + await MicrosoftUserRepository.DeleteAsync( + user.Id, + user.TenantId); + + // Assert + var exception = await Record.ExceptionAsync( + () => MicrosoftUserRepository.GetAsync( + user.Id, + user.TenantId)); + + // Assert + exception.Should().BeOfType(); + AssertExceptionMessageDoesNotContainPrefix(exception); + + var appleUser = await AppleUserRepository.GetAsync( + user.Id, + user.TenantId); + appleUser.Should().BeEquivalentTo(user); + } + + [Fact] + public async Task GetAsync_ShouldReturnSoftDeletedItemWhenSoftDeleteIsDisabledForMultipleTenants() + { + await ResetAsync(); + MicrosoftUserRepository.SoftDeleteState.Enable(); + + // Arrange + var user = User.Faker.Generate(); + user.IsDeleted.Should().BeFalse(); + await MicrosoftUserRepository.CreateAsync(user); + await AppleUserRepository.CreateAsync(user); + + // Act + await MicrosoftUserRepository.DeleteAsync( + user.Id, + user.TenantId); + MicrosoftUserRepository.SoftDeleteState.Disable(); + + // Assert + var userFromDb = await MicrosoftUserRepository.GetAsync( + user.Id, + user.TenantId); + + userFromDb.Should().BeEquivalentTo( + user, + options => options.Excluding(i => i.IsDeleted)); + userFromDb.IsDeleted.Should().BeTrue(); + } + + [Fact] + public async Task SoftDeleteByIdAndPartitionKeyShouldThrowIfNotExistingInMultiTenant() + { + // Arrange + MicrosoftUserRepository.SoftDeleteState.Enable(); + + // Act + var exception = await Record.ExceptionAsync( + () => MicrosoftUserRepository.DeleteAsync( + Guid.NewGuid().ToString(), + Guid.NewGuid().ToString())); + + // Assert + exception.Should().BeOfType(); + AssertExceptionMessageDoesNotContainPrefix(exception); + } + + [Fact] + public async Task SoftDeleteByIdShouldThrowIfNotExistingInMultiTenant() + { + // Arrange + MicrosoftUserRepository.SoftDeleteState.Enable(); + + // Act + var exception = await Record.ExceptionAsync( + () => MicrosoftUserRepository.DeleteAsync( + Guid.NewGuid().ToString())); + + // Assert + exception.Should().BeOfType(); + AssertExceptionMessageDoesNotContainPrefix(exception); + } +} diff --git a/src/core/Wemogy.Infrastructure.Database.Core.UnitTests/Repositories/RepositoryTestBase.DeleteAsync.cs b/src/core/Wemogy.Infrastructure.Database.Core.UnitTests/Repositories/RepositoryTestBase.DeleteAsync.cs index 98bf477..e1f345c 100644 --- a/src/core/Wemogy.Infrastructure.Database.Core.UnitTests/Repositories/RepositoryTestBase.DeleteAsync.cs +++ b/src/core/Wemogy.Infrastructure.Database.Core.UnitTests/Repositories/RepositoryTestBase.DeleteAsync.cs @@ -10,9 +10,10 @@ namespace Wemogy.Infrastructure.Database.Core.UnitTests.Repositories; public partial class RepositoryTestBase { [Fact] - public async Task DeleteAsyncShouldWork() + public async Task DeleteAsyncByIdAndPartitionKeyShouldWork() { // Arrange + await ResetAsync(); var user = User.Faker.Generate(); await MicrosoftUserRepository.CreateAsync(user); @@ -33,7 +34,47 @@ await MicrosoftUserRepository.DeleteAsync( } [Fact] - public async Task DeleteAsyncShouldThrowForNonExistingEntities() + public async Task DeleteAsyncByPredicateShouldWork() + { + // Arrange + await ResetAsync(); + var user = User.Faker.Generate(); + await MicrosoftUserRepository.CreateAsync(user); + + // Act + var userExistsBeforeDeletion = await MicrosoftUserRepository.ExistsAsync( + i => i.Id == user.Id && i.TenantId == user.TenantId); + var count = await MicrosoftUserRepository.DeleteAsync( + i => i.Id == user.Id && i.TenantId == user.TenantId); + var userExistsAfterDeletion = await MicrosoftUserRepository.ExistsAsync( + i => i.Id == user.Id && i.TenantId == user.TenantId); + + // Assert + userExistsBeforeDeletion.Should().BeTrue(); + userExistsAfterDeletion.Should().BeFalse(); + count.Should().Be(1); + } + + [Fact] + public async Task DeleteAsyncByIdShouldWork() + { + // Arrange + await ResetAsync(); + var user = User.Faker.Generate(); + await MicrosoftUserRepository.CreateAsync(user); + + // Act + var userExistsBeforeDeletion = await MicrosoftUserRepository.ExistsAsync(user.Id); + await MicrosoftUserRepository.DeleteAsync(user.Id); + var userExistsAfterDeletion = await MicrosoftUserRepository.ExistsAsync(user.Id); + + // Assert + userExistsBeforeDeletion.Should().BeTrue(); + userExistsAfterDeletion.Should().BeFalse(); + } + + [Fact] + public async Task DeleteAsyncByIdAndPartitionKeyShouldThrowForNonExistingEntities() { // Arrange var notExistingUserId = Guid.NewGuid().ToString(); @@ -50,17 +91,50 @@ public async Task DeleteAsyncShouldThrowForNonExistingEntities() } [Fact] - public async Task DeleteAsync_ShouldWork() + public async Task DeleteAsyncByPredicateShouldNotThrowForNonExistingEntities() + { + // Arrange + var notExistingUserId = Guid.NewGuid().ToString(); + var notExistingTenantId = Guid.NewGuid().ToString(); + + // Act + var count = await MicrosoftUserRepository.DeleteAsync( + i => i.Id == notExistingUserId && i.TenantId == notExistingTenantId); + + // Assert + count.Should().Be(0); + } + + [Fact] + public async Task DeleteAsyncByIdShouldThrowForNonExistingEntities() + { + // Arrange + var notExistingUserId = Guid.NewGuid().ToString(); + + // Act + var exception = await Record.ExceptionAsync( + () => MicrosoftUserRepository.DeleteAsync(notExistingUserId)); + + // Assert + exception.Should().BeOfType(); + } + + [Fact] + public async Task DeleteAllAsync_ShouldWork() { // Arrange await ResetAsync(); await MicrosoftUserRepository.CreateAsync(User.Faker.Generate()); + await MicrosoftUserRepository.CreateAsync(User.Faker.Generate()); + await MicrosoftUserRepository.CreateAsync(User.Faker.Generate()); + await MicrosoftUserRepository.CreateAsync(User.Faker.Generate()); // Act - await MicrosoftUserRepository.DeleteAsync(x => true); + var count = await MicrosoftUserRepository.DeleteAsync(x => true); // Assert var entities = await MicrosoftUserRepository.QueryAsync(x => true); entities.Should().BeEmpty(); + count.Should().Be(4); } } diff --git a/src/core/Wemogy.Infrastructure.Database.Core.UnitTests/Repositories/RepositoryTestBase.GetAllAsync.cs b/src/core/Wemogy.Infrastructure.Database.Core.UnitTests/Repositories/RepositoryTestBase.GetAllAsync.cs index 3c9f155..21eb3b2 100644 --- a/src/core/Wemogy.Infrastructure.Database.Core.UnitTests/Repositories/RepositoryTestBase.GetAllAsync.cs +++ b/src/core/Wemogy.Infrastructure.Database.Core.UnitTests/Repositories/RepositoryTestBase.GetAllAsync.cs @@ -28,6 +28,8 @@ public async Task GetAllAsync_ShouldReturnAllItems() [Fact] public async Task GetAllAsync_ShouldRespectSoftDeleteReturnAllItems() { + MicrosoftUserRepository.SoftDeleteState.Enable(); + // Arrange await ResetAsync(); var users = User.Faker.Generate(20); diff --git a/src/core/Wemogy.Infrastructure.Database.Core.UnitTests/Repositories/RepositoryTestBase.GetAsync.cs b/src/core/Wemogy.Infrastructure.Database.Core.UnitTests/Repositories/RepositoryTestBase.GetAsync.cs index 5fbc7c9..7dd8059 100644 --- a/src/core/Wemogy.Infrastructure.Database.Core.UnitTests/Repositories/RepositoryTestBase.GetAsync.cs +++ b/src/core/Wemogy.Infrastructure.Database.Core.UnitTests/Repositories/RepositoryTestBase.GetAsync.cs @@ -25,36 +25,6 @@ public async Task GetAsync_ShouldGetAnExistingItemById() userFromDb.Should().BeEquivalentTo(user); } - [Fact] - public async Task GetAsync_ShouldGetAnExistingItemByIdWithExpression() - { - // Arrange - await ResetAsync(); - var user = User.Faker.Generate(); - await MicrosoftUserRepository.CreateAsync(user); - - // Act - var userFromDb = await MicrosoftUserRepository.GetAsync(x => x.Id == user.Id); - - // Assert - userFromDb.Should().BeEquivalentTo(user); - } - - [Fact] - public async Task GetAsync_ShouldGetAnExistingItemByIdAndPartitionKeyWithExpression() - { - // Arrange - await ResetAsync(); - var user = User.Faker.Generate(); - await MicrosoftUserRepository.CreateAsync(user); - - // Act - var userFromDb = await MicrosoftUserRepository.GetAsync(x => x.Id == user.Id && x.TenantId == user.TenantId); - - // Assert - userFromDb.Should().BeEquivalentTo(user); - } - [Fact] public async Task GetAsync_ShouldThrowIfItemNotFound() { diff --git a/src/core/Wemogy.Infrastructure.Database.Core.UnitTests/Repositories/RepositoryTestBase.QueryAsync.cs b/src/core/Wemogy.Infrastructure.Database.Core.UnitTests/Repositories/RepositoryTestBase.QueryAsync.cs index 4a16f79..f5780ed 100644 --- a/src/core/Wemogy.Infrastructure.Database.Core.UnitTests/Repositories/RepositoryTestBase.QueryAsync.cs +++ b/src/core/Wemogy.Infrastructure.Database.Core.UnitTests/Repositories/RepositoryTestBase.QueryAsync.cs @@ -28,6 +28,8 @@ public async Task QueryAsync_ShouldReturnAllItemsIfEmptyQueryParameters() [Fact] public async Task QueryAsync_ShouldReturnAllNotDeletedItemsIfEmptyQueryParameters() { + MicrosoftUserRepository.SoftDeleteState.Enable(); + // Arrange await ResetAsync(); var queryParameters = new QueryParameters(); diff --git a/src/core/Wemogy.Infrastructure.Database.Core.UnitTests/Repositories/RepositoryTestBase.QuerySingleAsync.cs b/src/core/Wemogy.Infrastructure.Database.Core.UnitTests/Repositories/RepositoryTestBase.QuerySingleAsync.cs new file mode 100644 index 0000000..7be7855 --- /dev/null +++ b/src/core/Wemogy.Infrastructure.Database.Core.UnitTests/Repositories/RepositoryTestBase.QuerySingleAsync.cs @@ -0,0 +1,57 @@ +using System.Threading.Tasks; +using FluentAssertions; +using Wemogy.Core.Errors.Exceptions; +using Wemogy.Infrastructure.Database.Core.UnitTests.Fakes.Entities; +using Xunit; + +namespace Wemogy.Infrastructure.Database.Core.UnitTests.Repositories; + +public partial class RepositoryTestBase +{ + [Fact] + public async Task QuerySingleAsync_ShouldGetAnExistingItemByIdWithExpression() + { + // Arrange + await ResetAsync(); + var user = User.Faker.Generate(); + await MicrosoftUserRepository.CreateAsync(user); + + // Act + var userFromDb = await MicrosoftUserRepository.QuerySingleAsync(x => x.Id == user.Id); + + // Assert + userFromDb.Should().BeEquivalentTo(user); + } + + [Fact] + public async Task QuerySingleAsync_ShouldGetAnExistingItemByIdAndPartitionKeyWithExpression() + { + // Arrange + await ResetAsync(); + var user = User.Faker.Generate(); + await MicrosoftUserRepository.CreateAsync(user); + + // Act + var userFromDb = + await MicrosoftUserRepository.QuerySingleAsync(x => x.Id == user.Id && x.TenantId == user.TenantId); + + // Assert + userFromDb.Should().BeEquivalentTo(user); + } + + [Fact] + public async Task QuerySingleAsync_ShouldThrowWhenMoreThanOneResultsAreReturned() + { + // Arrange + await ResetAsync(); + await MicrosoftUserRepository.CreateAsync(User.Faker.Generate()); + await MicrosoftUserRepository.CreateAsync(User.Faker.Generate()); + + // Act + var exception = await Record.ExceptionAsync( + () => MicrosoftUserRepository.QuerySingleAsync(x => true)); + + // Assert + exception.Should().BeOfType(); + } +} diff --git a/src/core/Wemogy.Infrastructure.Database.Core.UnitTests/Repositories/RepositoryTestBase.SoftDelete.cs b/src/core/Wemogy.Infrastructure.Database.Core.UnitTests/Repositories/RepositoryTestBase.SoftDelete.cs index 62000ee..bb711ab 100644 --- a/src/core/Wemogy.Infrastructure.Database.Core.UnitTests/Repositories/RepositoryTestBase.SoftDelete.cs +++ b/src/core/Wemogy.Infrastructure.Database.Core.UnitTests/Repositories/RepositoryTestBase.SoftDelete.cs @@ -1,3 +1,4 @@ +using System; using System.Threading.Tasks; using FluentAssertions; using Wemogy.Core.Errors.Exceptions; @@ -9,36 +10,191 @@ namespace Wemogy.Infrastructure.Database.Core.UnitTests.Repositories; public partial class RepositoryTestBase { [Fact] - public async Task GetAsync_ShouldThrow_ForSoftDeletedItems() + public async Task GetAsync_ShouldThrow_ForSoftDeletedItemsByIdAndPartitionKey() { + MicrosoftUserRepository.SoftDeleteState.Enable(); + // Arrange var user = User.Faker.Generate(); - user.IsDeleted = true; + user.IsDeleted.Should().BeFalse(); await MicrosoftUserRepository.CreateAsync(user); - // Act & Assert - await Assert.ThrowsAsync( + // Act + await MicrosoftUserRepository.DeleteAsync( + user.Id, + user.TenantId); + + // Assert + var exception = await Record.ExceptionAsync( () => MicrosoftUserRepository.GetAsync( user.Id, user.TenantId)); + + // Assert + exception.Should().BeOfType(); + } + + [Fact] + public async Task GetAsync_ShouldThrow_ForSoftDeletedItemsById() + { + MicrosoftUserRepository.SoftDeleteState.Enable(); + + // Arrange + var user = User.Faker.Generate(); + user.IsDeleted.Should().BeFalse(); + await MicrosoftUserRepository.CreateAsync(user); + + // Act + await MicrosoftUserRepository.DeleteAsync(user.Id); + + // Assert + var exception = await Record.ExceptionAsync( + () => MicrosoftUserRepository.GetAsync(user.Id)); + + // Assert + exception.Should().BeOfType(); + } + + [Fact] + public async Task GetAsync_ShouldThrow_ForSoftDeletedItemsByPredicate() + { + MicrosoftUserRepository.SoftDeleteState.Enable(); + + // Arrange + var user = User.Faker.Generate(); + user.IsDeleted.Should().BeFalse(); + await MicrosoftUserRepository.CreateAsync(user); + + // Act + await MicrosoftUserRepository.DeleteAsync( + i => i.Id == user.Id && i.TenantId == user.TenantId); + + // Assert + var exception = await Record.ExceptionAsync( + () => MicrosoftUserRepository.GetAsync(user.Id)); + + // Assert + exception.Should().BeOfType(); } [Fact] public async Task GetAsync_ShouldReturnSoftDeletedItemWhenSoftDeleteIsDisabled() { + MicrosoftUserRepository.SoftDeleteState.Enable(); + // Arrange var user = User.Faker.Generate(); - user.IsDeleted = true; - MicrosoftUserRepository.SoftDelete.Disable(); + user.IsDeleted.Should().BeFalse(); await MicrosoftUserRepository.CreateAsync(user); // Act + await MicrosoftUserRepository.DeleteAsync( + user.Id, + user.TenantId); + MicrosoftUserRepository.SoftDeleteState.Disable(); + + // Assert var userFromDb = await MicrosoftUserRepository.GetAsync( user.Id, user.TenantId); + userFromDb.Should().BeEquivalentTo( + user, + options => options.Excluding(i => i.IsDeleted)); + userFromDb.IsDeleted.Should().BeTrue(); + } + + [Fact] + public async Task QuerySingleAsync_ShouldReturnSoftDeletedItemByPredicateWhenSoftDeleteIsDisabled() + { + MicrosoftUserRepository.SoftDeleteState.Enable(); + + // Arrange + var user = User.Faker.Generate(); + user.IsDeleted.Should().BeFalse(); + await MicrosoftUserRepository.CreateAsync(user); + + // Act + await MicrosoftUserRepository.DeleteAsync( + i => i.Id == user.Id && i.TenantId == user.TenantId); + MicrosoftUserRepository.SoftDeleteState.Disable(); + + // Assert + var userFromDb = await MicrosoftUserRepository.QuerySingleAsync( + i => i.Id == user.Id && i.TenantId == user.TenantId); + + userFromDb.Should().BeEquivalentTo( + user, + options => options.Excluding(i => i.IsDeleted)); + userFromDb.IsDeleted.Should().BeTrue(); + } + + [Fact] + public async Task GetAsync_ShouldReturnSoftDeletedItemByIdWhenSoftDeleteIsDisabled() + { + MicrosoftUserRepository.SoftDeleteState.Enable(); + + // Arrange + var user = User.Faker.Generate(); + user.IsDeleted.Should().BeFalse(); + await MicrosoftUserRepository.CreateAsync(user); + + // Act + await MicrosoftUserRepository.DeleteAsync(user.Id); + MicrosoftUserRepository.SoftDeleteState.Disable(); + + // Assert + var userFromDb = await MicrosoftUserRepository.GetAsync(user.Id); + + userFromDb.Should().BeEquivalentTo( + user, + options => options.Excluding(i => i.IsDeleted)); + userFromDb.IsDeleted.Should().BeTrue(); + } + + [Fact] + public async Task SoftDeleteByIdAndPartitionKeyShouldThrowIfNotExisting() + { + // Arrange + MicrosoftUserRepository.SoftDeleteState.Enable(); + + // Act + var exception = await Record.ExceptionAsync( + () => MicrosoftUserRepository.DeleteAsync( + Guid.NewGuid().ToString(), + Guid.NewGuid().ToString())); + + // Assert + exception.Should().BeOfType(); + } + + [Fact] + public async Task SoftDeleteByIdShouldThrowIfNotExisting() + { + // Arrange + MicrosoftUserRepository.SoftDeleteState.Enable(); + + // Act + var exception = await Record.ExceptionAsync( + () => MicrosoftUserRepository.DeleteAsync( + Guid.NewGuid().ToString())); + + // Assert + exception.Should().BeOfType(); + } + + [Fact] + public async Task SoftDeleteByPredicateShouldNotThrowIfNotExisting() + { + // Arrange + MicrosoftUserRepository.SoftDeleteState.Enable(); + + // Act + var exception = await Record.ExceptionAsync( + () => MicrosoftUserRepository.DeleteAsync( + i => i.Id == Guid.NewGuid().ToString() && i.TenantId == Guid.NewGuid().ToString())); + // Assert - Assert.True(userFromDb.IsDeleted); - userFromDb.Should().BeEquivalentTo(user); + exception.Should().BeNull(); } } diff --git a/src/core/Wemogy.Infrastructure.Database.Core/Abstractions/IDatabaseClient`1.cs b/src/core/Wemogy.Infrastructure.Database.Core/Abstractions/IDatabaseClient`1.cs index 0b6a621..73c9cd1 100644 --- a/src/core/Wemogy.Infrastructure.Database.Core/Abstractions/IDatabaseClient`1.cs +++ b/src/core/Wemogy.Infrastructure.Database.Core/Abstractions/IDatabaseClient`1.cs @@ -37,5 +37,5 @@ Task IterateAsync( Task DeleteAsync(string id, string partitionKey); - Task DeleteAsync(Expression> predicate); + Task DeleteAsync(Expression> predicate); } diff --git a/src/core/Wemogy.Infrastructure.Database.Core/Abstractions/IDatabaseRepository`1.Delete.cs b/src/core/Wemogy.Infrastructure.Database.Core/Abstractions/IDatabaseRepository`1.Delete.cs index afe72c5..f5ab97d 100644 --- a/src/core/Wemogy.Infrastructure.Database.Core/Abstractions/IDatabaseRepository`1.Delete.cs +++ b/src/core/Wemogy.Infrastructure.Database.Core/Abstractions/IDatabaseRepository`1.Delete.cs @@ -8,31 +8,36 @@ namespace Wemogy.Infrastructure.Database.Core.Abstractions; public partial interface IDatabaseRepository { /// - /// Delete an entity from the repository, as identified by its id. + /// Delete an entity from the repository, as identified by its id, or + /// set to soft-deleted instead if soft-delete is enabled. /// DO NOT PREFER: This method is less efficient, always prefer using an overload with predefined /// partitionKey. /// /// The unique identifier of the entity to delete - Task - DeleteAsync(string id); + /// + /// Thrown when the entity is not found (either because it does not exist or + /// because it has already been soft-deleted when this is supported) + /// + Task DeleteAsync(string id); /// - /// Delete an entity from the repository, as identified by its id and partitionKey. + /// Delete an entity from the repository, as identified by its id and partitionKey, or + /// set to soft-deleted instead if soft-delete is enabled. /// /// The unique identifier of the entity to delete /// The unique partitionKey of where the corresponding entity is located /// /// Thrown when the entity is not found (either because it does not exist or - /// because it has been soft-deleted when this is supported) + /// because it has already been soft-deleted when this is supported) /// - Task DeleteAsync(string id, string partitionKey); // TODO: make exception-throwing behaviour consistent, as seen in the other two methods + Task DeleteAsync(string id, string partitionKey); /// - /// Delete an entity from the repository, as identified by a given predicate. + /// Delete multiple entities from the repository, as identified by a given predicate, or + /// set to soft-deleted instead if soft-delete is enabled. /// ATTENTION: Including a filter for the partitionKey in the predicate improves performance drastically. /// /// The predicate to filter for the corresponding entity to delete. - Task - DeleteAsync( - Expression> predicate); + /// The number of (soft-)deleted entities, 0 if none + Task DeleteAsync(Expression> predicate); } diff --git a/src/core/Wemogy.Infrastructure.Database.Core/Abstractions/IDatabaseRepository`1.Get.cs b/src/core/Wemogy.Infrastructure.Database.Core/Abstractions/IDatabaseRepository`1.Get.cs index ecffc79..6dc9e25 100644 --- a/src/core/Wemogy.Infrastructure.Database.Core/Abstractions/IDatabaseRepository`1.Get.cs +++ b/src/core/Wemogy.Infrastructure.Database.Core/Abstractions/IDatabaseRepository`1.Get.cs @@ -1,5 +1,3 @@ -using System; -using System.Linq.Expressions; using System.Threading; using System.Threading.Tasks; using Wemogy.Core.Errors.Exceptions; @@ -9,7 +7,7 @@ namespace Wemogy.Infrastructure.Database.Core.Abstractions; public partial interface IDatabaseRepository { /// - /// Retrieve an entity from the repository by its unique identifier and partition key. + /// Retrieve a single entity from the repository by its unique identifier and partition key. /// /// The unique identifier of the entity to look for /// The unique partitionKey of where the corresponding entity should be located @@ -22,7 +20,7 @@ public partial interface IDatabaseRepository Task GetAsync(string id, string partitionKey, CancellationToken cancellationToken = default); /// - /// Retrieve an entity from the repository by its unique identifier. + /// Retrieve a single entity from the repository by its unique identifier. /// DO NOT PREFER: This method is less efficient, always prefer using an overload with predefined /// partitionKey. /// @@ -34,17 +32,4 @@ public partial interface IDatabaseRepository /// because it has been soft-deleted when this is supported) /// Task GetAsync(string id, CancellationToken cancellationToken = default); - - /// - /// Retrieve an entity from the repository by a predicate. - /// ATTENTION: Including a filter for the partitionKey in the predicate improves performance drastically. - /// - /// A filter of the entity to look for. - /// The cancellation token to use for the operation - /// The entity as found in the repository - /// - /// Thrown when the entity is not found (either because it does not exist or - /// because it has been soft-deleted when this is supported) - /// - Task GetAsync(Expression> predicate, CancellationToken cancellationToken = default); } diff --git a/src/core/Wemogy.Infrastructure.Database.Core/Abstractions/IDatabaseRepository`1.QuerySingle.cs b/src/core/Wemogy.Infrastructure.Database.Core/Abstractions/IDatabaseRepository`1.QuerySingle.cs new file mode 100644 index 0000000..ad9d988 --- /dev/null +++ b/src/core/Wemogy.Infrastructure.Database.Core/Abstractions/IDatabaseRepository`1.QuerySingle.cs @@ -0,0 +1,26 @@ +using System; +using System.Linq.Expressions; +using System.Threading; +using System.Threading.Tasks; +using Wemogy.Core.Errors.Exceptions; + +namespace Wemogy.Infrastructure.Database.Core.Abstractions; + +public partial interface IDatabaseRepository +{ + /// + /// Query the repository by a predicate for a SINGLE entity. Throws when the result is not single. + /// ATTENTION: Including a filter for the partitionKey in the predicate improves performance drastically. + /// + /// A filter of the entity to look for. + /// The cancellation token to use for the operation + /// The entity as found in the repository + /// + /// Thrown when the entity is not found (either because it does not exist or + /// because it has been soft-deleted when this is supported) + /// + /// + /// Thrown when more than results are found in the repository + /// + Task QuerySingleAsync(Expression> predicate, CancellationToken cancellationToken = default); +} diff --git a/src/core/Wemogy.Infrastructure.Database.Core/Abstractions/IDatabaseRepository`1.cs b/src/core/Wemogy.Infrastructure.Database.Core/Abstractions/IDatabaseRepository`1.cs index 46eba9b..1d3471d 100644 --- a/src/core/Wemogy.Infrastructure.Database.Core/Abstractions/IDatabaseRepository`1.cs +++ b/src/core/Wemogy.Infrastructure.Database.Core/Abstractions/IDatabaseRepository`1.cs @@ -13,5 +13,5 @@ public partial interface IDatabaseRepository : IDatabaseRepositoryBase /// /// Defines if soft-deletion is supported for the corresponding entity. /// - IEnabledState SoftDelete { get; } + IEnabledState SoftDeleteState { get; } } diff --git a/src/core/Wemogy.Infrastructure.Database.Core/Attributes/RepositoryPropertyFilterAttribute.cs b/src/core/Wemogy.Infrastructure.Database.Core/Attributes/RepositoryPropertyFilterAttribute.cs index 863af3a..319196f 100644 --- a/src/core/Wemogy.Infrastructure.Database.Core/Attributes/RepositoryPropertyFilterAttribute.cs +++ b/src/core/Wemogy.Infrastructure.Database.Core/Attributes/RepositoryPropertyFilterAttribute.cs @@ -2,6 +2,9 @@ namespace Wemogy.Infrastructure.Database.Core.Attributes; +/// +/// CAUTION! TODO: Ensure that it gets properly implemented +/// [AttributeUsage( AttributeTargets.Interface, AllowMultiple = true)] diff --git a/src/core/Wemogy.Infrastructure.Database.Core/Attributes/RepositoryReadFilterAttribute.cs b/src/core/Wemogy.Infrastructure.Database.Core/Attributes/RepositoryReadFilterAttribute.cs index 6c4482c..91dfcf9 100644 --- a/src/core/Wemogy.Infrastructure.Database.Core/Attributes/RepositoryReadFilterAttribute.cs +++ b/src/core/Wemogy.Infrastructure.Database.Core/Attributes/RepositoryReadFilterAttribute.cs @@ -2,6 +2,9 @@ namespace Wemogy.Infrastructure.Database.Core.Attributes; +/// +/// CAUTION! TODO: Ensure that it gets properly implemented +/// [AttributeUsage( AttributeTargets.Interface, AllowMultiple = true)] diff --git a/src/core/Wemogy.Infrastructure.Database.Core/Errors/DatabaseError.cs b/src/core/Wemogy.Infrastructure.Database.Core/Errors/DatabaseError.EntityNotFound.cs similarity index 94% rename from src/core/Wemogy.Infrastructure.Database.Core/Errors/DatabaseError.cs rename to src/core/Wemogy.Infrastructure.Database.Core/Errors/DatabaseError.EntityNotFound.cs index f0bd339..1838a48 100644 --- a/src/core/Wemogy.Infrastructure.Database.Core/Errors/DatabaseError.cs +++ b/src/core/Wemogy.Infrastructure.Database.Core/Errors/DatabaseError.EntityNotFound.cs @@ -3,7 +3,7 @@ namespace Wemogy.Infrastructure.Database.Core.Errors; -public static class DatabaseError +public static partial class DatabaseError { public static NotFoundErrorException EntityNotFound() { diff --git a/src/core/Wemogy.Infrastructure.Database.Core/Errors/DatabaseError.UnexpectedMultipleResults.cs b/src/core/Wemogy.Infrastructure.Database.Core/Errors/DatabaseError.UnexpectedMultipleResults.cs new file mode 100644 index 0000000..98a0b99 --- /dev/null +++ b/src/core/Wemogy.Infrastructure.Database.Core/Errors/DatabaseError.UnexpectedMultipleResults.cs @@ -0,0 +1,14 @@ +using Wemogy.Core.Errors; +using Wemogy.Core.Errors.Exceptions; + +namespace Wemogy.Infrastructure.Database.Core.Errors; + +public static partial class DatabaseError +{ + public static PreconditionFailedErrorException UnexpectedMultipleResults() + { + return Error.PreconditionFailed( + "UnexpectedMultipleResults", + "Querying for a single result returned more than one"); + } +} diff --git a/src/core/Wemogy.Infrastructure.Database.Core/Factories/DatabaseRepositoryFactory.cs b/src/core/Wemogy.Infrastructure.Database.Core/Factories/DatabaseRepositoryFactory.cs index ff965d5..b27030f 100644 --- a/src/core/Wemogy.Infrastructure.Database.Core/Factories/DatabaseRepositoryFactory.cs +++ b/src/core/Wemogy.Infrastructure.Database.Core/Factories/DatabaseRepositoryFactory.cs @@ -10,7 +10,7 @@ namespace Wemogy.Infrastructure.Database.Core.Factories; -public partial class DatabaseRepositoryFactory +public class DatabaseRepositoryFactory { private readonly IDatabaseClientFactory _databaseClientFactory; private readonly DatabaseRepositoryFactoryFactory _repositoryFactoryFactory; diff --git a/src/core/Wemogy.Infrastructure.Database.Core/Plugins/MultiTenantDatabase/Repositories/MultiTenantDatabaseRepository`1.Delete.cs b/src/core/Wemogy.Infrastructure.Database.Core/Plugins/MultiTenantDatabase/Repositories/MultiTenantDatabaseRepository`1.Delete.cs index bf98210..1910b6a 100644 --- a/src/core/Wemogy.Infrastructure.Database.Core/Plugins/MultiTenantDatabase/Repositories/MultiTenantDatabaseRepository`1.Delete.cs +++ b/src/core/Wemogy.Infrastructure.Database.Core/Plugins/MultiTenantDatabase/Repositories/MultiTenantDatabaseRepository`1.Delete.cs @@ -1,6 +1,7 @@ using System; using System.Linq.Expressions; using System.Threading.Tasks; +using Wemogy.Infrastructure.Database.Core.Errors; namespace Wemogy.Infrastructure.Database.Core.Plugins.MultiTenantDatabase.Repositories; @@ -10,13 +11,19 @@ public async Task DeleteAsync(string id) { try { - await _databaseRepository.DeleteAsync(IdAndPartitionKeyPrefixedPredicate(id)); + var isDeleted = await _databaseRepository.DeleteAsync(IdAndPartitionKeyPrefixedPredicate(id)); + if (isDeleted == 1) + { + return; + } } catch (Exception e) { CleanupException(e); throw; } + + throw DatabaseError.EntityNotFound(id); } public async Task DeleteAsync(string id, string partitionKey) @@ -34,12 +41,12 @@ await _databaseRepository.DeleteAsync( } } - public async Task DeleteAsync(Expression> predicate) + public async Task DeleteAsync(Expression> predicate) { try { predicate = BuildComposedPartitionKeyPredicate(predicate); - await _databaseRepository.DeleteAsync(predicate); + return await _databaseRepository.DeleteAsync(predicate); } catch (Exception e) { diff --git a/src/core/Wemogy.Infrastructure.Database.Core/Plugins/MultiTenantDatabase/Repositories/MultiTenantDatabaseRepository`1.Get.cs b/src/core/Wemogy.Infrastructure.Database.Core/Plugins/MultiTenantDatabase/Repositories/MultiTenantDatabaseRepository`1.Get.cs index 310282b..8df5036 100644 --- a/src/core/Wemogy.Infrastructure.Database.Core/Plugins/MultiTenantDatabase/Repositories/MultiTenantDatabaseRepository`1.Get.cs +++ b/src/core/Wemogy.Infrastructure.Database.Core/Plugins/MultiTenantDatabase/Repositories/MultiTenantDatabaseRepository`1.Get.cs @@ -1,5 +1,4 @@ using System; -using System.Linq.Expressions; using System.Threading; using System.Threading.Tasks; using Wemogy.Core.Errors.Exceptions; @@ -38,7 +37,7 @@ public async Task GetAsync(string id, CancellationToken cancellationTok { try { - var entity = await _databaseRepository.GetAsync( + var entity = await _databaseRepository.QuerySingleAsync( IdAndPartitionKeyPrefixedPredicate(id), cancellationToken); @@ -56,27 +55,4 @@ public async Task GetAsync(string id, CancellationToken cancellationTok throw; } } - - public async Task GetAsync( - Expression> predicate, - CancellationToken cancellationToken = default) - { - try - { - predicate = BuildComposedPartitionKeyPredicate(predicate); - - var entity = await _databaseRepository.GetAsync( - predicate, - cancellationToken); - - RemovePartitionKeyPrefix(entity); - - return entity; - } - catch (Exception e) - { - CleanupException(e); - throw; - } - } } diff --git a/src/core/Wemogy.Infrastructure.Database.Core/Plugins/MultiTenantDatabase/Repositories/MultiTenantDatabaseRepository`1.QuerySingle.cs b/src/core/Wemogy.Infrastructure.Database.Core/Plugins/MultiTenantDatabase/Repositories/MultiTenantDatabaseRepository`1.QuerySingle.cs new file mode 100644 index 0000000..2d2c50a --- /dev/null +++ b/src/core/Wemogy.Infrastructure.Database.Core/Plugins/MultiTenantDatabase/Repositories/MultiTenantDatabaseRepository`1.QuerySingle.cs @@ -0,0 +1,32 @@ +using System; +using System.Linq.Expressions; +using System.Threading; +using System.Threading.Tasks; + +namespace Wemogy.Infrastructure.Database.Core.Plugins.MultiTenantDatabase.Repositories; + +public partial class MultiTenantDatabaseRepository +{ + public async Task QuerySingleAsync( + Expression> predicate, + CancellationToken cancellationToken = default) + { + try + { + predicate = BuildComposedPartitionKeyPredicate(predicate); + + var entity = await _databaseRepository.QuerySingleAsync( + predicate, + cancellationToken); + + RemovePartitionKeyPrefix(entity); + + return entity; + } + catch (Exception e) + { + CleanupException(e); + throw; + } + } +} diff --git a/src/core/Wemogy.Infrastructure.Database.Core/Plugins/MultiTenantDatabase/Repositories/MultiTenantDatabaseRepository`1.cs b/src/core/Wemogy.Infrastructure.Database.Core/Plugins/MultiTenantDatabase/Repositories/MultiTenantDatabaseRepository`1.cs index 9a90243..1e348cb 100644 --- a/src/core/Wemogy.Infrastructure.Database.Core/Plugins/MultiTenantDatabase/Repositories/MultiTenantDatabaseRepository`1.cs +++ b/src/core/Wemogy.Infrastructure.Database.Core/Plugins/MultiTenantDatabase/Repositories/MultiTenantDatabaseRepository`1.cs @@ -27,7 +27,7 @@ public partial class MultiTenantDatabaseRepository : IDatabaseRepositor private Expression> PartitionKeyPredicate { get; } - public IEnabledState SoftDelete { get; } + public IEnabledState SoftDeleteState { get; } public MultiTenantDatabaseRepository( IDatabaseRepository databaseRepository, @@ -35,7 +35,7 @@ public MultiTenantDatabaseRepository( { _databaseRepository = databaseRepository; _databaseTenantProvider = databaseTenantProvider; - SoftDelete = databaseRepository.SoftDelete; + SoftDeleteState = databaseRepository.SoftDeleteState; _partitionKeyProperty = typeof(TEntity).GetPropertyByCustomAttribute() !; if (_partitionKeyProperty == null) { diff --git a/src/core/Wemogy.Infrastructure.Database.Core/Repositories/DatabaseRepository`1.Delete.cs b/src/core/Wemogy.Infrastructure.Database.Core/Repositories/DatabaseRepository`1.Delete.cs index 1a2a4d8..ac5ca6d 100644 --- a/src/core/Wemogy.Infrastructure.Database.Core/Repositories/DatabaseRepository`1.Delete.cs +++ b/src/core/Wemogy.Infrastructure.Database.Core/Repositories/DatabaseRepository`1.Delete.cs @@ -1,27 +1,65 @@ using System; +using System.Collections.Generic; using System.Linq.Expressions; using System.Threading.Tasks; using Wemogy.Infrastructure.Database.Core.Abstractions; +using Wemogy.Infrastructure.Database.Core.Errors; namespace Wemogy.Infrastructure.Database.Core.Repositories; public partial class DatabaseRepository where TEntity : class, IEntityBase { - public Task DeleteAsync(string id) + public async Task DeleteAsync(string id) { - return _database.DeleteAsync(x => id == x.Id.ToString()); + if (!SoftDeleteState.IsEnabled) + { + var isDeleted = await _database.DeleteAsync(x => id == x.Id.ToString()); + if (isDeleted == 1) + { + return; + } + + throw DatabaseError.EntityNotFound(id); + } + + await UpdateAsync( + id, + SoftDelete); } - public Task DeleteAsync(string id, string partitionKey) + public async Task DeleteAsync(string id, string partitionKey) { - return _database.DeleteAsync( + if (!SoftDeleteState.IsEnabled) + { + await _database.DeleteAsync( + id, + partitionKey); + return; + } + + await UpdateAsync( id, - partitionKey); + partitionKey, + SoftDelete); } - public Task DeleteAsync(Expression> predicate) + public async Task DeleteAsync(Expression> predicate) { - return _database.DeleteAsync(predicate); + if (!SoftDeleteState.IsEnabled) + { + return await _database.DeleteAsync(predicate); + } + + var entities = await QueryAsync(predicate); + var tasks = new List(); + foreach (var entity in entities) + { + SoftDelete(entity); + tasks.Add(_database.ReplaceAsync(entity)); + } + + await Task.WhenAll(tasks); + return entities.Count; } } diff --git a/src/core/Wemogy.Infrastructure.Database.Core/Repositories/DatabaseRepository`1.Get.cs b/src/core/Wemogy.Infrastructure.Database.Core/Repositories/DatabaseRepository`1.Get.cs index 8df7c88..ba5a9d1 100644 --- a/src/core/Wemogy.Infrastructure.Database.Core/Repositories/DatabaseRepository`1.Get.cs +++ b/src/core/Wemogy.Infrastructure.Database.Core/Repositories/DatabaseRepository`1.Get.cs @@ -23,7 +23,7 @@ public async Task GetAsync( cancellationToken); // Throw exception if soft delete is enabled and entity is deleted - if (SoftDelete.IsEnabled && IsSoftDeleted(entity)) + if (SoftDeleteState.IsEnabled && IsSoftDeleted(entity)) { throw DatabaseError.EntityNotFound( id, @@ -75,7 +75,7 @@ public async Task GetAsync( var entity = items.First(); // Throw exception if soft delete is enabled and entity is deleted - if (SoftDelete.IsEnabled && IsSoftDeleted(entity)) + if (SoftDeleteState.IsEnabled && IsSoftDeleted(entity)) { throw DatabaseError.EntityNotFound(predicate.ToString()); } diff --git a/src/core/Wemogy.Infrastructure.Database.Core/Repositories/DatabaseRepository`1.Iterate.cs b/src/core/Wemogy.Infrastructure.Database.Core/Repositories/DatabaseRepository`1.Iterate.cs index 278ab74..111121d 100644 --- a/src/core/Wemogy.Infrastructure.Database.Core/Repositories/DatabaseRepository`1.Iterate.cs +++ b/src/core/Wemogy.Infrastructure.Database.Core/Repositories/DatabaseRepository`1.Iterate.cs @@ -15,7 +15,7 @@ public Task IterateAsync( Func callback, CancellationToken cancellationToken = default) { - if (SoftDelete.IsEnabled) + if (SoftDeleteState.IsEnabled) { predicate = predicate.And(_softDeleteFilterExpression); } @@ -35,7 +35,7 @@ public Task IterateAsync( { Expression>? predicate = null; - if (SoftDelete.IsEnabled) + if (SoftDeleteState.IsEnabled) { predicate = _softDeleteFilterExpression; } diff --git a/src/core/Wemogy.Infrastructure.Database.Core/Repositories/DatabaseRepository`1.QuerySingle.cs b/src/core/Wemogy.Infrastructure.Database.Core/Repositories/DatabaseRepository`1.QuerySingle.cs new file mode 100644 index 0000000..46450e9 --- /dev/null +++ b/src/core/Wemogy.Infrastructure.Database.Core/Repositories/DatabaseRepository`1.QuerySingle.cs @@ -0,0 +1,44 @@ +using System; +using System.Linq; +using System.Linq.Expressions; +using System.Threading; +using System.Threading.Tasks; +using Wemogy.Infrastructure.Database.Core.Abstractions; +using Wemogy.Infrastructure.Database.Core.Errors; + +namespace Wemogy.Infrastructure.Database.Core.Repositories; + +public partial class DatabaseRepository + where TEntity : class, IEntityBase +{ + public async Task QuerySingleAsync( + Expression> predicate, + CancellationToken cancellationToken = default) + { + var items = await QueryAsync( + predicate, + cancellationToken); + + if (!items.Any()) + { + throw DatabaseError.EntityNotFound(predicate.ToString()); + } + + if (items.Count > 1) + { + throw DatabaseError.UnexpectedMultipleResults(); + } + + var entity = items.First(); + + // Throw exception if soft delete is enabled and entity is deleted + if (SoftDeleteState.IsEnabled && IsSoftDeleted(entity)) + { + throw DatabaseError.EntityNotFound(predicate.ToString()); + } + + await PropertyFilters.ApplyAsync(entity); + + return entity; + } +} diff --git a/src/core/Wemogy.Infrastructure.Database.Core/Repositories/DatabaseRepository`1.cs b/src/core/Wemogy.Infrastructure.Database.Core/Repositories/DatabaseRepository`1.cs index 50e377c..76c01c4 100644 --- a/src/core/Wemogy.Infrastructure.Database.Core/Repositories/DatabaseRepository`1.cs +++ b/src/core/Wemogy.Infrastructure.Database.Core/Repositories/DatabaseRepository`1.cs @@ -47,7 +47,7 @@ public DatabaseRepository( { _database = database; _readFilters = readFilters; - SoftDelete = softDeleteState; + SoftDeleteState = softDeleteState; PropertyFilters = new PropertyFiltersState( true, propertyFilters); @@ -87,7 +87,7 @@ public DatabaseRepository( } public PropertyFiltersState PropertyFilters { get; } - public IEnabledState SoftDelete { get; } + public IEnabledState SoftDeleteState { get; } private bool IsSoftDeleted(TEntity entity) { @@ -99,6 +99,18 @@ private bool IsSoftDeleted(TEntity entity) return (bool)_softDeleteFlagProperty.GetValue(entity); } + private void SoftDelete(TEntity entity) + { + if (_softDeleteFlagProperty == null) + { + return; + } + + _softDeleteFlagProperty.SetValue( + entity, + true); + } + private async Task> GetReadFilter() { Expression> defaultFilter = x => true; diff --git a/src/core/Wemogy.Infrastructure.Database.Core/Wemogy.Infrastructure.Database.Core.csproj b/src/core/Wemogy.Infrastructure.Database.Core/Wemogy.Infrastructure.Database.Core.csproj index d82050f..1d80261 100644 --- a/src/core/Wemogy.Infrastructure.Database.Core/Wemogy.Infrastructure.Database.Core.csproj +++ b/src/core/Wemogy.Infrastructure.Database.Core/Wemogy.Infrastructure.Database.Core.csproj @@ -158,5 +158,25 @@ content Compile + + cs + content + Compile + + + cs + content + Compile + + + cs + content + Compile + + + cs + content + Compile + diff --git a/src/cosmos/Wemogy.Infrastructure.Database.Cosmos/Client/CosmosDatabaseClient`1.cs b/src/cosmos/Wemogy.Infrastructure.Database.Cosmos/Client/CosmosDatabaseClient`1.cs index a471242..7b00be2 100644 --- a/src/cosmos/Wemogy.Infrastructure.Database.Cosmos/Client/CosmosDatabaseClient`1.cs +++ b/src/cosmos/Wemogy.Infrastructure.Database.Cosmos/Client/CosmosDatabaseClient`1.cs @@ -145,9 +145,10 @@ public Task DeleteAsync(string id, string partitionKey) new PartitionKey(partitionKey)); } - public Task DeleteAsync(Expression> predicate) + public async Task DeleteAsync(Expression> predicate) { - return IterateAsync( + var count = 0; + await IterateAsync( predicate, async entity => { @@ -156,7 +157,10 @@ public Task DeleteAsync(Expression> predicate) await DeleteAsync( id, partitionKey); + count++; }); + + return count; } private async Task DeleteAsync(string id, PartitionKey partitionKey) diff --git a/src/in-memory/Wemogy.Infrastructure.Database.InMemory/Client/InMemoryDatabaseClient`1.cs b/src/in-memory/Wemogy.Infrastructure.Database.InMemory/Client/InMemoryDatabaseClient`1.cs index 4c86c94..405d7ae 100644 --- a/src/in-memory/Wemogy.Infrastructure.Database.InMemory/Client/InMemoryDatabaseClient`1.cs +++ b/src/in-memory/Wemogy.Infrastructure.Database.InMemory/Client/InMemoryDatabaseClient`1.cs @@ -46,7 +46,9 @@ public Task GetAsync(string id, string partitionKey, CancellationToken partitionKey); } - TEntity entity = entities.AsQueryable().FirstOrDefault("e => e.Id.Equals(@0)", id); + TEntity entity = entities.AsQueryable().FirstOrDefault( + "e => e.Id.Equals(@0)", + id); if (entity == null) { @@ -116,7 +118,9 @@ public Task CreateAsync(TEntity entity) entities); } - if (entities.AsQueryable().Any("x => x.Id.Equals(@0)", id)) + if (entities.AsQueryable().Any( + "x => x.Id.Equals(@0)", + id)) { throw Error.Conflict( "AlreadyExists", @@ -141,7 +145,9 @@ public Task ReplaceAsync(TEntity entity) partitionKeyValue); } - var existingEntity = entities.AsQueryable().FirstOrDefault("e => e.Id.Equals(@0)", id); + var existingEntity = entities.AsQueryable().FirstOrDefault( + "e => e.Id.Equals(@0)", + id); if (existingEntity == null) { @@ -167,7 +173,9 @@ public Task DeleteAsync(string id, string partitionKey) partitionKey); } - var entity = entities.AsQueryable().FirstOrDefault("e => e.Id.Equals(@0)", id); + var entity = entities.AsQueryable().FirstOrDefault( + "e => e.Id.Equals(@0)", + id); if (entity == null) { @@ -180,8 +188,9 @@ public Task DeleteAsync(string id, string partitionKey) return Task.CompletedTask; } - public Task DeleteAsync(Expression> predicate) + public Task DeleteAsync(Expression> predicate) { + int count = 0; var compiledPredicate = predicate.Compile(); foreach (var entityPartition in EntityPartitions) { @@ -190,10 +199,11 @@ public Task DeleteAsync(Expression> predicate) foreach (var entity in entities) { entityPartition.Value.Remove(entity); + count++; } } - return Task.CompletedTask; + return Task.FromResult(count); } } }