diff --git a/src/core/Wemogy.Infrastructure.Database.Core.UnitTests/Repositories/RepositoryTestBase.UpsertAsync.cs b/src/core/Wemogy.Infrastructure.Database.Core.UnitTests/Repositories/RepositoryTestBase.UpsertAsync.cs new file mode 100644 index 0000000..eaa2d71 --- /dev/null +++ b/src/core/Wemogy.Infrastructure.Database.Core.UnitTests/Repositories/RepositoryTestBase.UpsertAsync.cs @@ -0,0 +1,152 @@ +using System; +using System.Threading.Tasks; +using FluentAssertions; +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 UpsertAsync_ShouldCreateIfNotExist() + { + // Arrange + await ResetAsync(); + var user = User.Faker.Generate(); + + // Act + Exception? exception = null; + User? updatedUser = null; + + try + { + updatedUser = await MicrosoftUserRepository.UpsertAsync( + user, + user.TenantId); + } + catch (Exception ex) + { + exception = ex; + } + + // Assert + if (exception is NotSupportedException) + { + // Expected outcome in certain implementations + return; + } + + exception.Should().BeNull(); + updatedUser.Should().NotBeNull(); + updatedUser.Id.Should().Be(user.Id); + updatedUser.TenantId.Should().Be(user.TenantId); + } + + [Fact] + public async Task UpsertAsync_ShouldReplaceIfExist() + { + // Arrange + await ResetAsync(); + var user = User.Faker.Generate(); + await MicrosoftUserRepository.CreateAsync(user); + user.Firstname = "Updated"; + + // Act + Exception? exception = null; + User? updatedUser = null; + + try + { + updatedUser = await MicrosoftUserRepository.UpsertAsync( + user, + user.TenantId); + } + catch (Exception ex) + { + exception = ex; + } + + // Assert + if (exception is NotSupportedException) + { + // Expected outcome in certain implementations + return; + } + + exception.Should().BeNull(); + updatedUser.Should().NotBeNull(); + updatedUser.Firstname.Should().Be("Updated"); + updatedUser.Id.Should().Be(user.Id); + updatedUser.TenantId.Should().Be(user.TenantId); + } + + [Fact] + public async Task UpsertAsyncWithoutPartitionKey_ShouldCreateIfNotExist() + { + // Arrange + await ResetAsync(); + var user = User.Faker.Generate(); + + // Act + Exception? exception = null; + User? updatedUser = null; + + try + { + updatedUser = await MicrosoftUserRepository.UpsertAsync(user); + } + catch (Exception ex) + { + exception = ex; + } + + // Assert + if (exception is NotSupportedException) + { + // Expected outcome in certain implementations + return; + } + + exception.Should().BeNull(); + updatedUser.Should().NotBeNull(); + updatedUser.Id.Should().Be(user.Id); + updatedUser.TenantId.Should().Be(user.TenantId); + } + + [Fact] + public async Task UpsertAsyncWithoutPartitionKey_ShouldReplaceIfExist() + { + // Arrange + await ResetAsync(); + var user = User.Faker.Generate(); + await MicrosoftUserRepository.CreateAsync(user); + user.Firstname = "Updated"; + + // Act + Exception? exception = null; + User? updatedUser = null; + + try + { + updatedUser = await MicrosoftUserRepository.UpsertAsync(user); + } + catch (Exception ex) + { + exception = ex; + } + + // Assert + if (exception is NotSupportedException) + { + // Expected outcome in certain implementations + return; + } + + exception.Should().BeNull(); + updatedUser.Should().NotBeNull(); + updatedUser.Firstname.Should().Be("Updated"); + updatedUser.Id.Should().Be(user.Id); + updatedUser.TenantId.Should().Be(user.TenantId); + } +} 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 849db88..ee7e5ff 100644 --- a/src/core/Wemogy.Infrastructure.Database.Core/Abstractions/IDatabaseClient`1.cs +++ b/src/core/Wemogy.Infrastructure.Database.Core/Abstractions/IDatabaseClient`1.cs @@ -47,4 +47,8 @@ Task CountAsync( Task DeleteAsync(string id, string partitionKey); Task DeleteAsync(Expression> predicate); + + Task UpsertAsync(TEntity entity); + + Task UpsertAsync(TEntity entity, string partitionKey); } diff --git a/src/core/Wemogy.Infrastructure.Database.Core/Abstractions/IDatabaseRepository`1.Upsert.cs b/src/core/Wemogy.Infrastructure.Database.Core/Abstractions/IDatabaseRepository`1.Upsert.cs new file mode 100644 index 0000000..f1956c5 --- /dev/null +++ b/src/core/Wemogy.Infrastructure.Database.Core/Abstractions/IDatabaseRepository`1.Upsert.cs @@ -0,0 +1,24 @@ +using System.Threading.Tasks; + +namespace Wemogy.Infrastructure.Database.Core.Abstractions; + +/// +/// Defines methods for upserting entities in a database repository. +/// +public partial interface IDatabaseRepository +{ + /// + /// Upsert an entity in the database. + /// + /// The entity to upsert + /// The upserted entity as persisted + Task UpsertAsync(TEntity entity); + + /// + /// Upsert an entity in the database, using the specified partition key. + /// + /// The entity to upsert. + /// The partition key to use for the upsert operation. + /// The upserted entity as persisted + Task UpsertAsync(TEntity entity, string partitionKey); +} diff --git a/src/core/Wemogy.Infrastructure.Database.Core/Plugins/MultiTenantDatabase/Repositories/MultiTenantDatabaseRepository`1.Upsert.cs b/src/core/Wemogy.Infrastructure.Database.Core/Plugins/MultiTenantDatabase/Repositories/MultiTenantDatabaseRepository`1.Upsert.cs new file mode 100644 index 0000000..13d4922 --- /dev/null +++ b/src/core/Wemogy.Infrastructure.Database.Core/Plugins/MultiTenantDatabase/Repositories/MultiTenantDatabaseRepository`1.Upsert.cs @@ -0,0 +1,30 @@ +using System.Threading.Tasks; + +namespace Wemogy.Infrastructure.Database.Core.Plugins.MultiTenantDatabase.Repositories; + +/// +/// Repository for handling multi-tenant database operations for . +/// +public partial class MultiTenantDatabaseRepository +{ + /// + /// Inserts or updates the specified entity in the database. + /// + /// The entity to upsert. + /// The upserted entity. + public Task UpsertAsync(TEntity entity) + { + return _databaseRepository.UpsertAsync(entity); + } + + /// + /// Inserts or updates the specified entity in the database using the provided partition key. + /// + /// The entity to upsert. + /// The partition key to use for the operation. + /// The upserted entity. + public Task UpsertAsync(TEntity entity, string partitionKey) + { + return _databaseRepository.UpsertAsync(entity, partitionKey); + } +} diff --git a/src/core/Wemogy.Infrastructure.Database.Core/Repositories/DatabaseRepository`1.Upsert.cs b/src/core/Wemogy.Infrastructure.Database.Core/Repositories/DatabaseRepository`1.Upsert.cs new file mode 100644 index 0000000..cecd0c4 --- /dev/null +++ b/src/core/Wemogy.Infrastructure.Database.Core/Repositories/DatabaseRepository`1.Upsert.cs @@ -0,0 +1,35 @@ +using System.Threading.Tasks; +using Wemogy.Infrastructure.Database.Core.Abstractions; + +namespace Wemogy.Infrastructure.Database.Core.Repositories; + +/// +/// Represents a repository for performing database operations on entities of type . +/// +/// The type of the entity. +public partial class DatabaseRepository + where TEntity : class, IEntityBase +{ + /// + /// Inserts or updates the specified entity in the database using the provided partition key. + /// + /// The entity to upsert. + /// The partition key for the entity. + /// A task that represents the asynchronous operation. The task result contains the upserted entity. + public Task UpsertAsync(TEntity entity, string partitionKey) + { + return _database.UpsertAsync( + entity, + partitionKey); + } + + /// + /// Inserts or updates the specified entity in the database. + /// + /// The entity to upsert. + /// A task that represents the asynchronous operation. The task result contains the upserted entity. + public Task UpsertAsync(TEntity entity) + { + return _database.UpsertAsync(entity); + } +} 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 8d5a583..92dbe34 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 @@ -175,5 +175,20 @@ 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 f62048a..ca02510 100644 --- a/src/cosmos/Wemogy.Infrastructure.Database.Cosmos/Client/CosmosDatabaseClient`1.cs +++ b/src/cosmos/Wemogy.Infrastructure.Database.Cosmos/Client/CosmosDatabaseClient`1.cs @@ -167,6 +167,33 @@ public async Task ReplaceAsync(TEntity entity) } } + public async Task UpsertAsync(TEntity entity) + { + var partitionKey = ResolvePartitionKey(entity); + var upsertResponse = await _container.UpsertItemAsync( + entity, + partitionKey.CosmosPartitionKey, + new ItemRequestOptions + { + EnableContentResponseOnWrite = true + }); + + return upsertResponse.Resource; + } + + public async Task UpsertAsync(TEntity entity, string partitionKey) + { + var upsertResponse = await _container.UpsertItemAsync( + entity, + new PartitionKey(partitionKey).CosmosPartitionKey, + new ItemRequestOptions + { + EnableContentResponseOnWrite = true + }); + + return upsertResponse.Resource; + } + public Task DeleteAsync(string id, string partitionKey) { return DeleteAsync( 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 b694617..e7fddeb 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 @@ -197,6 +197,56 @@ public Task ReplaceAsync(TEntity entity) return Task.FromResult(entity.Clone()); } + public Task UpsertAsync(TEntity entity) + { + var id = ResolveIdValue(entity); + var partitionKeyValue = ResolvePartitionKeyValue(entity); + + if (!EntityPartitions.TryGetValue( + partitionKeyValue, + out var entities)) + { + entities = new List(); + EntityPartitions.Add( + partitionKeyValue, + entities); + } + + var existingEntity = entities.AsQueryable().FirstOrDefault("e => e.Id.Equals(@0)", id); + + if (existingEntity != null) + { + entities.Remove(existingEntity); + } + + entities.Add(entity.Clone()); + return Task.FromResult(entity.Clone()); + } + + public Task UpsertAsync(TEntity entity, string partitionKey) + { + if (!EntityPartitions.TryGetValue( + partitionKey, + out var entities)) + { + entities = new List(); + EntityPartitions.Add( + partitionKey, + entities); + } + + var id = ResolveIdValue(entity); + var existingEntity = entities.AsQueryable().FirstOrDefault("e => e.Id.Equals(@0)", id); + + if (existingEntity != null) + { + entities.Remove(existingEntity); + } + + entities.Add(entity.Clone()); + return Task.FromResult(entity.Clone()); + } + public Task DeleteAsync(string id, string partitionKey) { if (!EntityPartitions.TryGetValue( diff --git a/src/mongo/Wemogy.Infrastructure.Database.Mongo/Client/MongoDatabaseClient`1.cs b/src/mongo/Wemogy.Infrastructure.Database.Mongo/Client/MongoDatabaseClient`1.cs index ab4bcd8..ca085dd 100644 --- a/src/mongo/Wemogy.Infrastructure.Database.Mongo/Client/MongoDatabaseClient`1.cs +++ b/src/mongo/Wemogy.Infrastructure.Database.Mongo/Client/MongoDatabaseClient`1.cs @@ -185,6 +185,16 @@ public async Task ReplaceAsync(TEntity entity) return entity; } + public Task UpsertAsync(TEntity entity) + { + throw new NotSupportedException(); + } + + public Task UpsertAsync(TEntity entity, string partitionKey) + { + throw new NotSupportedException(); + } + public async Task DeleteAsync(string id, string partitionKey) { var filter = GetEntityFilterDefinition(