From 973528679f90bbd2a985041726b0b0d872d2209f Mon Sep 17 00:00:00 2001 From: Tudor Stanciu <63559907+tstanciu@users.noreply.github.com> Date: Thu, 6 Feb 2025 17:56:50 +0200 Subject: [PATCH] MultiTenant entities - Get tenant from context only if there is any modified multitenant entity (#280) * MultiTenant entities - Get tenant from context only if there is any modified multitenant entity * MultiTenant entities - Get tenant from context only if there is any modified multitenant entity * MultiTenant entities - Get tenant from context only if there is any modified multitenant entity * MultiTenant entities - Get tenant from context only if there is any modified multitenant entity * MultiTenant entities - Get tenant from context only if there is any modified multitenant entity --------- Co-authored-by: Tudor Stanciu --- .../DbContextExtensions.cs | 10 +++- .../Context/TenantContextExtensions.cs | 6 +- .../MultiTenancyTests.cs | 55 +++++++++++++++---- .../SimpleEntity.cs | 10 ++++ .../SimpleEntityConfiguration.cs | 16 ++++++ .../TestDbContext.cs | 2 + 6 files changed, 82 insertions(+), 17 deletions(-) create mode 100644 test/UnitTests/Data/NBB.Data.EntityFramework.MultiTenancy.Tests/SimpleEntity.cs create mode 100644 test/UnitTests/Data/NBB.Data.EntityFramework.MultiTenancy.Tests/SimpleEntityConfiguration.cs diff --git a/src/Data/NBB.Data.EntityFramework.MultiTenancy/DbContextExtensions.cs b/src/Data/NBB.Data.EntityFramework.MultiTenancy/DbContextExtensions.cs index dd39a91a..93fb7417 100644 --- a/src/Data/NBB.Data.EntityFramework.MultiTenancy/DbContextExtensions.cs +++ b/src/Data/NBB.Data.EntityFramework.MultiTenancy/DbContextExtensions.cs @@ -14,12 +14,16 @@ public static class DbContextExtensions { public static void SetTenantIdFromContext(this DbContext context) { - var tenantId = context.GetTenantIdFromContext(); - var multiTenantEntities = context.ChangeTracker.Entries() .Where(e => e.IsMultiTenant() && e.State != EntityState.Unchanged); + if (!multiTenantEntities.Any()) + { + return; + } + + var tenantId = context.GetTenantIdFromContext(); foreach (var e in multiTenantEntities) { var attemptedTenantId = e.GetTenantId(); @@ -41,4 +45,4 @@ public static void UseMultitenancy(this DbContextOptionsBuilder options, IServic ((IDbContextOptionsBuilderInfrastructure)options).AddOrUpdateExtension(extension); } } -} \ No newline at end of file +} diff --git a/src/MultiTenancy/NBB.MultiTenancy.Abstractions/Context/TenantContextExtensions.cs b/src/MultiTenancy/NBB.MultiTenancy.Abstractions/Context/TenantContextExtensions.cs index d7fdf259..59797703 100644 --- a/src/MultiTenancy/NBB.MultiTenancy.Abstractions/Context/TenantContextExtensions.cs +++ b/src/MultiTenancy/NBB.MultiTenancy.Abstractions/Context/TenantContextExtensions.cs @@ -7,9 +7,9 @@ namespace NBB.MultiTenancy.Abstractions.Context { public static class TenantContextExtensions { - public static Guid GetTenantId(this TenantContext tenantContext) => tenantContext.Tenant?.TenantId ?? throw new TenantNotFoundException(); - public static Guid? TryGetTenantId(this TenantContext tenantContext) => tenantContext.Tenant?.TenantId; - public static string GetTenantCode(this TenantContext tenantContext) => tenantContext.Tenant?.Code ?? throw new TenantNotFoundException(); + public static Guid GetTenantId(this TenantContext tenantContext) => tenantContext?.Tenant?.TenantId ?? throw new TenantNotFoundException(); + public static Guid? TryGetTenantId(this TenantContext tenantContext) => tenantContext?.Tenant?.TenantId; + public static string GetTenantCode(this TenantContext tenantContext) => tenantContext?.Tenant?.Code ?? throw new TenantNotFoundException(); public static TenantContextFlow ChangeTenantContext(this ITenantContextAccessor tenantContextAccessor, Tenant tenant) => tenantContextAccessor.ChangeTenantContext(new TenantContext(tenant)); diff --git a/test/UnitTests/Data/NBB.Data.EntityFramework.MultiTenancy.Tests/MultiTenancyTests.cs b/test/UnitTests/Data/NBB.Data.EntityFramework.MultiTenancy.Tests/MultiTenancyTests.cs index df3a6e0b..28a897f3 100644 --- a/test/UnitTests/Data/NBB.Data.EntityFramework.MultiTenancy.Tests/MultiTenancyTests.cs +++ b/test/UnitTests/Data/NBB.Data.EntityFramework.MultiTenancy.Tests/MultiTenancyTests.cs @@ -1,10 +1,6 @@ // Copyright (c) TotalSoft. // This source code is licensed under the MIT license. -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; using FluentAssertions; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; @@ -15,6 +11,10 @@ using NBB.MultiTenancy.Abstractions.Configuration; using NBB.MultiTenancy.Abstractions.Context; using NBB.MultiTenancy.Abstractions.Options; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; using Xunit; namespace NBB.Data.EntityFramework.MultiTenancy.Tests @@ -26,7 +26,7 @@ public async Task Should_add_tenantId() { // arrange var testTenantId = Guid.NewGuid(); - var sp = GetServiceProvider(true); + var sp = GetServiceProvider(); var testEntity = new TestEntity { Id = 1 }; await WithTenantScope(sp, testTenantId, async sp => @@ -48,7 +48,7 @@ public async Task Should_Exception_Be_Thrown_If_Different_TenantIds() { // arrange var testTenantId = Guid.NewGuid(); - var sp = GetServiceProvider(true); + var sp = GetServiceProvider(); var testEntity = new TestEntity { Id = 1 }; var testEntityOtherId = new TestEntity { Id = 2 }; await WithTenantScope(sp, testTenantId, async sp => @@ -75,7 +75,7 @@ public async Task Shoud_Apply_Filter() var testTenantId2 = Guid.NewGuid(); var testEntity = new TestEntity { Id = 1 }; var testEntityOtherId = new TestEntity { Id = 2 }; - var sp = GetServiceProvider(true); + var sp = GetServiceProvider(); await WithTenantScope(sp, testTenantId1, async sp => { @@ -109,7 +109,7 @@ public async Task Should_add_TenantId_and_filter_for_MultiTenantContext() { // arrange var testTenantId = Guid.NewGuid(); - var sp = GetServiceProvider(true); + var sp = GetServiceProvider(); var testEntity = new TestEntity { Id = 1 }; var testEntity1 = new TestEntity { Id = 2 }; @@ -133,13 +133,44 @@ await WithTenantScope(sp, testTenantId, async sp => }); } - private IServiceProvider GetServiceProvider(bool isSharedDB) where TDBContext : DbContext + [Fact] + public async Task Can_Save_MultiTenantDbContext_WO_TennatContext_When_Only_NonMultiTenant_Entities_Changed() + { + // arrange + var sp = GetServiceProvider(DbStrategy.Shared); + var testEntity = new SimpleEntity { Id = 1 }; + var testEntityOtherId = new SimpleEntity { Id = 2 }; + + var dbContext = sp.GetRequiredService(); + + dbContext.SimpleEntities.Add(testEntity); + dbContext.SimpleEntities.Add(testEntityOtherId); + + // act + var count = await dbContext.SaveChangesAsync(); + + // assert + count.Should().Be(2); + } + + enum DbStrategy + { + DatabasePerTenant, + Shared, + Hybrid + } + + private IServiceProvider GetServiceProvider(DbStrategy dbStrategy = DbStrategy.Hybrid) where TDBContext : DbContext { var tenantService = Mock.Of(x => x.TenantContext == null); + var isSharedDB = dbStrategy == DbStrategy.Shared; + var isHybridDB = dbStrategy == DbStrategy.Hybrid; + var connectionStringKey = isSharedDB ? "ConnectionStrings:myDb" : "MultiTenancy:Defaults:ConnectionStrings:myDb"; + var connectionStringValue = isSharedDB || isHybridDB ? "Test" : Guid.NewGuid().ToString(); IConfiguration configuration = new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary { - { "MultiTenancy:Defaults:ConnectionStrings:myDb", isSharedDB ? "Test" : Guid.NewGuid().ToString()} + { connectionStringKey, connectionStringValue } }) .Build(); @@ -156,7 +187,9 @@ private IServiceProvider GetServiceProvider(bool isSharedDB) where T services.AddEntityFrameworkInMemoryDatabase() .AddDbContext((sp, options) => { - var conn = sp.GetRequiredService().GetConnectionString("myDb"); + var conn = isSharedDB ? + configuration.GetConnectionString("myDb") : + sp.GetRequiredService().GetConnectionString("myDb"); options.UseInMemoryDatabase(conn).UseInternalServiceProvider(sp); }); diff --git a/test/UnitTests/Data/NBB.Data.EntityFramework.MultiTenancy.Tests/SimpleEntity.cs b/test/UnitTests/Data/NBB.Data.EntityFramework.MultiTenancy.Tests/SimpleEntity.cs new file mode 100644 index 00000000..3016c71e --- /dev/null +++ b/test/UnitTests/Data/NBB.Data.EntityFramework.MultiTenancy.Tests/SimpleEntity.cs @@ -0,0 +1,10 @@ +// Copyright (c) TotalSoft. +// This source code is licensed under the MIT license. + +namespace NBB.Data.EntityFramework.MultiTenancy.Tests +{ + public class SimpleEntity + { + public int Id { get; set; } + } +} diff --git a/test/UnitTests/Data/NBB.Data.EntityFramework.MultiTenancy.Tests/SimpleEntityConfiguration.cs b/test/UnitTests/Data/NBB.Data.EntityFramework.MultiTenancy.Tests/SimpleEntityConfiguration.cs new file mode 100644 index 00000000..4c21f01f --- /dev/null +++ b/test/UnitTests/Data/NBB.Data.EntityFramework.MultiTenancy.Tests/SimpleEntityConfiguration.cs @@ -0,0 +1,16 @@ +// Copyright (c) TotalSoft. +// This source code is licensed under the MIT license. + +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace NBB.Data.EntityFramework.MultiTenancy.Tests +{ + public class SimpleEntityConfiguration : IEntityTypeConfiguration + { + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(x => x.Id); + } + } +} diff --git a/test/UnitTests/Data/NBB.Data.EntityFramework.MultiTenancy.Tests/TestDbContext.cs b/test/UnitTests/Data/NBB.Data.EntityFramework.MultiTenancy.Tests/TestDbContext.cs index e6004016..0b99bb5b 100644 --- a/test/UnitTests/Data/NBB.Data.EntityFramework.MultiTenancy.Tests/TestDbContext.cs +++ b/test/UnitTests/Data/NBB.Data.EntityFramework.MultiTenancy.Tests/TestDbContext.cs @@ -8,6 +8,7 @@ namespace NBB.Data.EntityFramework.MultiTenancy.Tests public class TestDbContext : MultiTenantDbContext { public DbSet TestEntities { get; set; } + public DbSet SimpleEntities { get; set; } public TestDbContext(DbContextOptions options) : base(options) { @@ -16,6 +17,7 @@ public TestDbContext(DbContextOptions options) : base(options) protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.ApplyConfiguration(new TestEntityConfiguration()); + modelBuilder.ApplyConfiguration(new SimpleEntityConfiguration()); base.OnModelCreating(modelBuilder); }