diff --git a/Masa.Contrib.sln b/Masa.Contrib.sln index 90cd0517a..67e9e4980 100644 --- a/Masa.Contrib.sln +++ b/Masa.Contrib.sln @@ -130,11 +130,23 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Masa.BuildingBlocks.SearchE EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Masa.BuildingBlocks.Service.MinimalAPIs", "src\BuildingBlocks\MASA.BuildingBlocks\src\Service\Masa.BuildingBlocks.Service.MinimalAPIs\Masa.BuildingBlocks.Service.MinimalAPIs.csproj", "{E72E105D-B15F-4D69-9A13-CAA49D4889D6}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Masa.Contrib.Dispatcher.Events.HandlerOrder.Tests", "test\Masa.Contrib.Dispatcher.Events.HandlerOrder.Tests\Masa.Contrib.Dispatcher.Events.HandlerOrder.Tests.csproj", "{4A052E17-4D9E-41EF-89A5-73B917053F8E}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Masa.Contrib.Dispatcher.Events.HandlerOrder.Tests", "test\Masa.Contrib.Dispatcher.Events.HandlerOrder.Tests\Masa.Contrib.Dispatcher.Events.HandlerOrder.Tests.csproj", "{4A052E17-4D9E-41EF-89A5-73B917053F8E}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Masa.Contrib.SearchEngine.AutoComplete", "src\SearchEngine\Masa.Contrib.SearchEngine.AutoComplete\Masa.Contrib.SearchEngine.AutoComplete.csproj", "{3F8532EF-3DC9-45F8-9562-994ABE066585}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Masa.Contrib.SearchEngine.AutoComplete", "src\SearchEngine\Masa.Contrib.SearchEngine.AutoComplete\Masa.Contrib.SearchEngine.AutoComplete.csproj", "{3F8532EF-3DC9-45F8-9562-994ABE066585}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Masa.Contrib.SearchEngine.AutoComplete.Tests", "test\Masa.Contrib.SearchEngine.AutoComplete.Tests\Masa.Contrib.SearchEngine.AutoComplete.Tests.csproj", "{31262D61-26A4-4302-968D-52B8DA4558CD}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Masa.Contrib.SearchEngine.AutoComplete.Tests", "test\Masa.Contrib.SearchEngine.AutoComplete.Tests\Masa.Contrib.SearchEngine.AutoComplete.Tests.csproj", "{31262D61-26A4-4302-968D-52B8DA4558CD}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Masa.BuildingBlocks.Isolation", "src\BuildingBlocks\MASA.BuildingBlocks\src\Isolation\Masa.BuildingBlocks.Isolation\Masa.BuildingBlocks.Isolation.csproj", "{B689E82B-B3E8-4C83-B56C-D4C27206AAC6}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Masa.Contrib.Isolation.UoW.EF", "src\Isolation\Masa.Contrib.Isolation.UoW.EF\Masa.Contrib.Isolation.UoW.EF.csproj", "{0C25062D-60A5-4690-974A-CDA9619866B4}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Masa.Contrib.Isolation.MultiTenant", "src\Isolation\Masa.Contrib.Isolation.MultiTenant\Masa.Contrib.Isolation.MultiTenant.csproj", "{6D9A3F62-8AB6-40AD-B32F-2A435A9D3341}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Masa.Contrib.Isolation.Environment", "src\Isolation\Masa.Contrib.Isolation.Environment\Masa.Contrib.Isolation.Environment.csproj", "{E9A3C62C-EA85-40C3-BB3B-7836D0337F63}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Masa.Contrib.Isolation.UoW.EF.Tests", "test\Masa.Contrib.Isolation.UoW.EF.Tests\Masa.Contrib.Isolation.UoW.EF.Tests.csproj", "{50551732-AAE0-4F77-B401-9DD02E226CDB}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Masa.Contrib.Isolation.UoW.EF.Web.Tests", "test\Masa.Contrib.Isolation.UoW.EF.Web.Tests\Masa.Contrib.Isolation.UoW.EF.Web.Tests.csproj", "{7CF33D08-9468-464D-B52E-955A5667EE42}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -504,6 +516,54 @@ Global {31262D61-26A4-4302-968D-52B8DA4558CD}.Release|Any CPU.Build.0 = Release|Any CPU {31262D61-26A4-4302-968D-52B8DA4558CD}.Release|x64.ActiveCfg = Release|Any CPU {31262D61-26A4-4302-968D-52B8DA4558CD}.Release|x64.Build.0 = Release|Any CPU + {B689E82B-B3E8-4C83-B56C-D4C27206AAC6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B689E82B-B3E8-4C83-B56C-D4C27206AAC6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B689E82B-B3E8-4C83-B56C-D4C27206AAC6}.Debug|x64.ActiveCfg = Debug|Any CPU + {B689E82B-B3E8-4C83-B56C-D4C27206AAC6}.Debug|x64.Build.0 = Debug|Any CPU + {B689E82B-B3E8-4C83-B56C-D4C27206AAC6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B689E82B-B3E8-4C83-B56C-D4C27206AAC6}.Release|Any CPU.Build.0 = Release|Any CPU + {B689E82B-B3E8-4C83-B56C-D4C27206AAC6}.Release|x64.ActiveCfg = Release|Any CPU + {B689E82B-B3E8-4C83-B56C-D4C27206AAC6}.Release|x64.Build.0 = Release|Any CPU + {0C25062D-60A5-4690-974A-CDA9619866B4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0C25062D-60A5-4690-974A-CDA9619866B4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0C25062D-60A5-4690-974A-CDA9619866B4}.Debug|x64.ActiveCfg = Debug|Any CPU + {0C25062D-60A5-4690-974A-CDA9619866B4}.Debug|x64.Build.0 = Debug|Any CPU + {0C25062D-60A5-4690-974A-CDA9619866B4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0C25062D-60A5-4690-974A-CDA9619866B4}.Release|Any CPU.Build.0 = Release|Any CPU + {0C25062D-60A5-4690-974A-CDA9619866B4}.Release|x64.ActiveCfg = Release|Any CPU + {0C25062D-60A5-4690-974A-CDA9619866B4}.Release|x64.Build.0 = Release|Any CPU + {6D9A3F62-8AB6-40AD-B32F-2A435A9D3341}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6D9A3F62-8AB6-40AD-B32F-2A435A9D3341}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6D9A3F62-8AB6-40AD-B32F-2A435A9D3341}.Debug|x64.ActiveCfg = Debug|Any CPU + {6D9A3F62-8AB6-40AD-B32F-2A435A9D3341}.Debug|x64.Build.0 = Debug|Any CPU + {6D9A3F62-8AB6-40AD-B32F-2A435A9D3341}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6D9A3F62-8AB6-40AD-B32F-2A435A9D3341}.Release|Any CPU.Build.0 = Release|Any CPU + {6D9A3F62-8AB6-40AD-B32F-2A435A9D3341}.Release|x64.ActiveCfg = Release|Any CPU + {6D9A3F62-8AB6-40AD-B32F-2A435A9D3341}.Release|x64.Build.0 = Release|Any CPU + {E9A3C62C-EA85-40C3-BB3B-7836D0337F63}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E9A3C62C-EA85-40C3-BB3B-7836D0337F63}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E9A3C62C-EA85-40C3-BB3B-7836D0337F63}.Debug|x64.ActiveCfg = Debug|Any CPU + {E9A3C62C-EA85-40C3-BB3B-7836D0337F63}.Debug|x64.Build.0 = Debug|Any CPU + {E9A3C62C-EA85-40C3-BB3B-7836D0337F63}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E9A3C62C-EA85-40C3-BB3B-7836D0337F63}.Release|Any CPU.Build.0 = Release|Any CPU + {E9A3C62C-EA85-40C3-BB3B-7836D0337F63}.Release|x64.ActiveCfg = Release|Any CPU + {E9A3C62C-EA85-40C3-BB3B-7836D0337F63}.Release|x64.Build.0 = Release|Any CPU + {50551732-AAE0-4F77-B401-9DD02E226CDB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {50551732-AAE0-4F77-B401-9DD02E226CDB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {50551732-AAE0-4F77-B401-9DD02E226CDB}.Debug|x64.ActiveCfg = Debug|Any CPU + {50551732-AAE0-4F77-B401-9DD02E226CDB}.Debug|x64.Build.0 = Debug|Any CPU + {50551732-AAE0-4F77-B401-9DD02E226CDB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {50551732-AAE0-4F77-B401-9DD02E226CDB}.Release|Any CPU.Build.0 = Release|Any CPU + {50551732-AAE0-4F77-B401-9DD02E226CDB}.Release|x64.ActiveCfg = Release|Any CPU + {50551732-AAE0-4F77-B401-9DD02E226CDB}.Release|x64.Build.0 = Release|Any CPU + {7CF33D08-9468-464D-B52E-955A5667EE42}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7CF33D08-9468-464D-B52E-955A5667EE42}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7CF33D08-9468-464D-B52E-955A5667EE42}.Debug|x64.ActiveCfg = Debug|Any CPU + {7CF33D08-9468-464D-B52E-955A5667EE42}.Debug|x64.Build.0 = Debug|Any CPU + {7CF33D08-9468-464D-B52E-955A5667EE42}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7CF33D08-9468-464D-B52E-955A5667EE42}.Release|Any CPU.Build.0 = Release|Any CPU + {7CF33D08-9468-464D-B52E-955A5667EE42}.Release|x64.ActiveCfg = Release|Any CPU + {7CF33D08-9468-464D-B52E-955A5667EE42}.Release|x64.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -571,6 +631,12 @@ Global {4A052E17-4D9E-41EF-89A5-73B917053F8E} = {2BE750A5-8AC7-457C-9BB2-6E3D5E2D23B8} {3F8532EF-3DC9-45F8-9562-994ABE066585} = {8C39C640-0E8A-43A7-890C-9742B6B70AA4} {31262D61-26A4-4302-968D-52B8DA4558CD} = {38E6C400-90C0-493E-9266-C1602E229F1B} + {B689E82B-B3E8-4C83-B56C-D4C27206AAC6} = {DC578D74-98F0-4F19-A230-CFA8DAEE0AF1} + {0C25062D-60A5-4690-974A-CDA9619866B4} = {022D6FF5-4B65-4213-9A97-C69E2B2F99E1} + {6D9A3F62-8AB6-40AD-B32F-2A435A9D3341} = {022D6FF5-4B65-4213-9A97-C69E2B2F99E1} + {E9A3C62C-EA85-40C3-BB3B-7836D0337F63} = {022D6FF5-4B65-4213-9A97-C69E2B2F99E1} + {50551732-AAE0-4F77-B401-9DD02E226CDB} = {38E6C400-90C0-493E-9266-C1602E229F1B} + {7CF33D08-9468-464D-B52E-955A5667EE42} = {38E6C400-90C0-493E-9266-C1602E229F1B} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {40383055-CC50-4600-AD9A-53C14F620D03} diff --git a/src/BuildingBlocks/MASA.BuildingBlocks b/src/BuildingBlocks/MASA.BuildingBlocks index ed8277ca0..0a6e41874 160000 --- a/src/BuildingBlocks/MASA.BuildingBlocks +++ b/src/BuildingBlocks/MASA.BuildingBlocks @@ -1 +1 @@ -Subproject commit ed8277ca02b1259277836f34568719bd37bf705a +Subproject commit 0a6e41874a315d2d46ede9873ae5d674ffbb409d diff --git a/src/Data/Masa.Contrib.Data.UoW.EF/DispatcherOptionsExtensions.cs b/src/Data/Masa.Contrib.Data.UoW.EF/DispatcherOptionsExtensions.cs index 5f38ec2f5..338a53833 100644 --- a/src/Data/Masa.Contrib.Data.UoW.EF/DispatcherOptionsExtensions.cs +++ b/src/Data/Masa.Contrib.Data.UoW.EF/DispatcherOptionsExtensions.cs @@ -40,7 +40,7 @@ private static IServiceCollection UseUoW( services.AddSingleton(); services.TryAddScoped(); - services.TryAddSingleton(); + services.TryAddSingleton>(); services.TryAddScoped(); services.TryAddSingleton(); diff --git a/src/Data/Masa.Contrib.Data.UoW.EF/UnitOfWork.cs b/src/Data/Masa.Contrib.Data.UoW.EF/UnitOfWork.cs index ff9d5cdad..3a25e74c8 100644 --- a/src/Data/Masa.Contrib.Data.UoW.EF/UnitOfWork.cs +++ b/src/Data/Masa.Contrib.Data.UoW.EF/UnitOfWork.cs @@ -4,7 +4,9 @@ public class UnitOfWork : IUnitOfWork where TDbContext : MasaDbConte { public IServiceProvider ServiceProvider { get; } - protected DbContext Context; + private readonly DbContext? _context = null; + + protected DbContext Context => _context ?? ServiceProvider.GetRequiredService(); public DbTransaction Transaction { @@ -30,11 +32,7 @@ public DbTransaction Transaction public bool UseTransaction { get; set; } = true; - public UnitOfWork(IServiceProvider serviceProvider) - { - ServiceProvider = serviceProvider; - Context = serviceProvider.GetRequiredService(); - } + public UnitOfWork(IServiceProvider serviceProvider) => ServiceProvider = serviceProvider; public async Task SaveChangesAsync(CancellationToken cancellationToken = default) { @@ -59,7 +57,7 @@ public async Task RollbackAsync(CancellationToken cancellationToken = default) await Context.Database.RollbackTransactionAsync(cancellationToken); } - public async ValueTask DisposeAsync() => await Context.DisposeAsync(); + public async ValueTask DisposeAsync() => await (_context?.DisposeAsync() ?? ValueTask.CompletedTask); - public void Dispose() => Context.Dispose(); + public void Dispose() => _context?.Dispose(); } diff --git a/src/Data/Masa.Contrib.Data.UoW.EF/UnitOfWorkManager.cs b/src/Data/Masa.Contrib.Data.UoW.EF/UnitOfWorkManager.cs index 8edadd18d..9376ba11e 100644 --- a/src/Data/Masa.Contrib.Data.UoW.EF/UnitOfWorkManager.cs +++ b/src/Data/Masa.Contrib.Data.UoW.EF/UnitOfWorkManager.cs @@ -1,14 +1,23 @@ namespace Masa.Contrib.Data.UoW.EF; -public class UnitOfWorkManager : IUnitOfWorkManager +public class UnitOfWorkManager : IUnitOfWorkManager where TDbContext : MasaDbContext { private readonly IServiceProvider _serviceProvider; public UnitOfWorkManager(IServiceProvider serviceProvider) => _serviceProvider = serviceProvider; - public IUnitOfWork CreateDbContext() + /// + /// Create new DbContext + /// We create DbContext with lazy loading enabled by default + /// + /// Deferred creation of DbContext, easy to specify tenant or environment by yourself, which is very effective for physical isolation + /// + public IUnitOfWork CreateDbContext(bool lazyLoading = true) { var scope = _serviceProvider.CreateAsyncScope(); + if (!lazyLoading) + scope.ServiceProvider.GetRequiredService(); + return scope.ServiceProvider.GetRequiredService(); } @@ -21,6 +30,7 @@ public IUnitOfWork CreateDbContext(MasaDbContextConfigurationOptions options) var scope = _serviceProvider.CreateAsyncScope(); var unitOfWorkAccessor = scope.ServiceProvider.GetRequiredService(); unitOfWorkAccessor.CurrentDbContextOptions = options; + return scope.ServiceProvider.GetRequiredService(); } } diff --git a/src/Dispatcher/Masa.Contrib.Dispatcher.IntegrationEvents.Dapr/ServiceCollectionExtensions.cs b/src/Dispatcher/Masa.Contrib.Dispatcher.IntegrationEvents.Dapr/ServiceCollectionExtensions.cs index eb585e51b..131da625f 100644 --- a/src/Dispatcher/Masa.Contrib.Dispatcher.IntegrationEvents.Dapr/ServiceCollectionExtensions.cs +++ b/src/Dispatcher/Masa.Contrib.Dispatcher.IntegrationEvents.Dapr/ServiceCollectionExtensions.cs @@ -47,7 +47,7 @@ internal static IServiceCollection TryAddDaprEventBus service.ServiceType != typeof(IUnitOfWork))) { var logger = services.BuildServiceProvider().GetService>(); - logger?.LogWarning("UoW is not enabled, local messages will not be integrated"); + logger?.LogDebug("UoW is not enabled or add delay, UoW is not used will affect 100% delivery of the message"); } return services; diff --git a/src/Isolation/Masa.Contrib.Isolation.Environment/EnvironmentContext.cs b/src/Isolation/Masa.Contrib.Isolation.Environment/EnvironmentContext.cs new file mode 100644 index 000000000..696d4491e --- /dev/null +++ b/src/Isolation/Masa.Contrib.Isolation.Environment/EnvironmentContext.cs @@ -0,0 +1,8 @@ +namespace Masa.Contrib.Isolation.Environment; + +public class EnvironmentContext : IEnvironmentContext, IEnvironmentSetter +{ + public string CurrentEnvironment { get; private set; } = string.Empty; + + public void SetEnvironment(string environment) => CurrentEnvironment = environment; +} diff --git a/src/Isolation/Masa.Contrib.Isolation.Environment/EnvironmentSaveChangesFilter.cs b/src/Isolation/Masa.Contrib.Isolation.Environment/EnvironmentSaveChangesFilter.cs new file mode 100644 index 000000000..10adea2b5 --- /dev/null +++ b/src/Isolation/Masa.Contrib.Isolation.Environment/EnvironmentSaveChangesFilter.cs @@ -0,0 +1,23 @@ +namespace Masa.Contrib.Isolation.Environment; + +public class EnvironmentSaveChangesFilter: ISaveChangesFilter +{ + private readonly IEnvironmentContext _environmentContext; + + public EnvironmentSaveChangesFilter(IEnvironmentContext environmentContext) + { + _environmentContext = environmentContext; + } + + public void OnExecuting(ChangeTracker changeTracker) + { + changeTracker.DetectChanges(); + foreach (var entity in changeTracker.Entries().Where(entry => entry.State == EntityState.Added)) + { + if (entity.Entity is IMultiEnvironment) + { + entity.CurrentValues[nameof(IMultiEnvironment.Environment)] = _environmentContext.CurrentEnvironment; + } + } + } +} diff --git a/src/Isolation/Masa.Contrib.Isolation.Environment/IsolationBuilderExtensions.cs b/src/Isolation/Masa.Contrib.Isolation.Environment/IsolationBuilderExtensions.cs new file mode 100644 index 000000000..fed35bef8 --- /dev/null +++ b/src/Isolation/Masa.Contrib.Isolation.Environment/IsolationBuilderExtensions.cs @@ -0,0 +1,13 @@ +namespace Masa.Contrib.Isolation.Environment; + +public static class IsolationBuilderExtensions +{ + public static IIsolationBuilder UseEnvironment(this IIsolationBuilder isolationBuilder) + { + isolationBuilder.Services.TryAddEnumerable(new ServiceDescriptor(typeof(ISaveChangesFilter), typeof(EnvironmentSaveChangesFilter), ServiceLifetime.Scoped)); + isolationBuilder.Services.TryAddScoped(); + isolationBuilder.Services.TryAddScoped(typeof(IEnvironmentContext), serviceProvider => serviceProvider.GetRequiredService()); + isolationBuilder.Services.TryAddScoped(typeof(IEnvironmentSetter), serviceProvider => serviceProvider.GetRequiredService()); + return isolationBuilder; + } +} diff --git a/src/Isolation/Masa.Contrib.Isolation.Environment/Masa.Contrib.Isolation.Environment.csproj b/src/Isolation/Masa.Contrib.Isolation.Environment/Masa.Contrib.Isolation.Environment.csproj new file mode 100644 index 000000000..9f22c786e --- /dev/null +++ b/src/Isolation/Masa.Contrib.Isolation.Environment/Masa.Contrib.Isolation.Environment.csproj @@ -0,0 +1,17 @@ + + + + net6.0 + enable + enable + + + + + + + + + + + diff --git a/src/Isolation/Masa.Contrib.Isolation.Environment/README.md b/src/Isolation/Masa.Contrib.Isolation.Environment/README.md new file mode 100644 index 000000000..85bb35334 --- /dev/null +++ b/src/Isolation/Masa.Contrib.Isolation.Environment/README.md @@ -0,0 +1,88 @@ +[中](README.zh-CN.md) | EN + +## Masa.Contrib.Isolation.Environment + +Example: + +```C# +Install-Package Masa.Contrib.Isolation.UoW.EF +Install-Package Masa.Contrib.Isolation.Environment +Install-Package Masa.Utils.Data.EntityFrameworkCore.SqlServer +``` + +1. 配置appsettings.json +``` appsettings.json +{ + "ConnectionStrings": { + "DefaultConnection": "server=localhost;uid=sa;pwd=P@ssw0rd;database=identity;", + "Isolations": [ + { + "Environment": "development", + "ConnectionString": "server=localhost,1674;uid=sa;pwd=P@ssw0rd;database=identity;" + }, + { + "Environment": "staging", + "ConnectionString": "server=localhost,1672;uid=sa;pwd=P@ssw0rd;database=identity;" + } + ] + } +} +``` +* 1.1 When the current environment is development: database address: server=localhost,1674;uid=sa;pwd=P@ssw0rd;database=identity; +* 1.2 When the current environment is staging: database address: server=localhost,1672;uid=sa;pwd=P@ssw0rd;database=identity; +* 1.3 When the current environment is another environment: database address: server=localhost;uid=sa;pwd=P@ssw0rd;database=identity; + +2. Using Isolation.UoW.EF +```` C# +builder.Services.AddEventBus(eventBusBuilder => +{ + eventBusBuilder.UseIsolationUoW( + dbOptions => dbOptions.UseSqlServer(), + isolationBuilder => isolationBuilder.UseEnvironment()); +}); +```` + +3. DbContext needs to inherit IsolationDbContext + +```` C# +public class CustomDbContext : IsolationDbContext +{ + public CustomDbContext(MasaDbContextOptions options) : base(options) + { + } +} +```` + +4. The class corresponding to the isolated table needs to implement IMultiEnvironment + +You can also choose not to implement IMultiEnvironment when using physical isolation + +##### Summarize + +* How is the environment resolved in the controller or MinimalAPI? + * The environment provides one parser by default, and the execution order is: EnvironmentVariablesParserProvider (gets the parameters in the system environment variables, the parameters of the default environment isolation: ASPNETCORE_ENVIRONMENT) +* If the parser fails to resolve the environment, what is the last database used? + * If the parsing environment fails, return DefaultConnection directly +* How to change the default environment parameter name + +```` C# +builder.Services.AddEventBus(eventBusBuilder => +{ + eventBusBuilder.UseIsolationUoW( + dbOptions => dbOptions.UseSqlServer(), + isolationBuilder => isolationBuilder.SetEnvironmentKey("env").UseEnvironment());// Use environment isolation +}); +```` +* How to change the parser + +```` C# +builder.Services.AddEventBus(eventBusBuilder => +{ + eventBusBuilder.UseIsolationUoW( + dbOptions => dbOptions.UseSqlServer(), + isolationBuilder => isolationBuilder.SetEnvironmentParsers(new List() + { + new EnvironmentVariablesParserProvider() //By default, environment information in environment isolation is obtained from system environment variables + }).UseEnvironment());// Use environment isolation +}); +```` \ No newline at end of file diff --git a/src/Isolation/Masa.Contrib.Isolation.Environment/README.zh-CN.md b/src/Isolation/Masa.Contrib.Isolation.Environment/README.zh-CN.md new file mode 100644 index 000000000..72f0ffa27 --- /dev/null +++ b/src/Isolation/Masa.Contrib.Isolation.Environment/README.zh-CN.md @@ -0,0 +1,89 @@ +中 | [EN](README.md) + +## Masa.Contrib.Isolation.Environment + +用例: + +```C# +Install-Package Masa.Contrib.Isolation.UoW.EF +Install-Package Masa.Contrib.Isolation.Environment +Install-Package Masa.Utils.Data.EntityFrameworkCore.SqlServer +``` + +1. 配置appsettings.json +``` appsettings.json +{ + "ConnectionStrings": { + "DefaultConnection": "server=localhost;uid=sa;pwd=P@ssw0rd;database=identity;", + "Isolations": [ + { + "Environment": "development", + "ConnectionString": "server=localhost,1674;uid=sa;pwd=P@ssw0rd;database=identity;" + }, + { + "Environment": "staging", + "ConnectionString": "server=localhost,1672;uid=sa;pwd=P@ssw0rd;database=identity;" + } + ] + } +} +``` + +* 1.1 当前环境是development时:数据库地址:server=localhost,1674;uid=sa;pwd=P@ssw0rd;database=identity; +* 1.2 当前环境是staging时:数据库地址:server=localhost,1672;uid=sa;pwd=P@ssw0rd;database=identity; +* 1.3 当前环境是其他环境时:数据库地址:server=localhost;uid=sa;pwd=P@ssw0rd;database=identity; + +2. 使用Isolation.UoW.EF +``` C# +builder.Services.AddEventBus(eventBusBuilder => +{ + eventBusBuilder.UseIsolationUoW( + dbOptions => dbOptions.UseSqlServer(), + isolationBuilder => isolationBuilder.UseEnvironment()); +}); +``` + +3. DbContext需要继承IsolationDbContext + +``` C# +public class CustomDbContext : IsolationDbContext +{ + public CustomDbContext(MasaDbContextOptions options) : base(options) + { + } +} +``` + +4. 隔离的表对应的类需要实现IMultiEnvironment + +采用物理隔离时也可以选择不实现IMultiEnvironment + +##### 总结 + +* 控制器或MinimalAPI中环境如何解析? + * 环境默认提供了1个解析器,执行顺序为:EnvironmentVariablesParserProvider (获取系统环境变量中的参数,默认环境隔离的参数:ASPNETCORE_ENVIRONMENT) +* 如果解析器解析环境失败,那最后使用的数据库是? + * 若解析环境失败,则直接返回DefaultConnection +* 如何更改默认环境参数名 + +``` C# +builder.Services.AddEventBus(eventBusBuilder => +{ + eventBusBuilder.UseIsolationUoW( + dbOptions => dbOptions.UseSqlServer(), + isolationBuilder => isolationBuilder.SetEnvironmentKey("env").UseEnvironment());// 使用环境隔离 +}); +``` +* 如何更改解析器 + +``` C# +builder.Services.AddEventBus(eventBusBuilder => +{ + eventBusBuilder.UseIsolationUoW( + dbOptions => dbOptions.UseSqlServer(), + isolationBuilder => isolationBuilder.SetEnvironmentParsers(new List() + { + new EnvironmentVariablesParserProvider() // 默认从系统环境变量中获取环境隔离中的环境信息 + }).UseEnvironment());// 使用环境隔离 +}); +``` \ No newline at end of file diff --git a/src/Isolation/Masa.Contrib.Isolation.Environment/_Imports.cs b/src/Isolation/Masa.Contrib.Isolation.Environment/_Imports.cs new file mode 100644 index 000000000..aa48cf9b7 --- /dev/null +++ b/src/Isolation/Masa.Contrib.Isolation.Environment/_Imports.cs @@ -0,0 +1,7 @@ +global using Masa.BuildingBlocks.Isolation; +global using Masa.BuildingBlocks.Isolation.Environment; +global using Masa.Utils.Data.EntityFrameworkCore.Filters; +global using Microsoft.EntityFrameworkCore; +global using Microsoft.EntityFrameworkCore.ChangeTracking; +global using Microsoft.Extensions.DependencyInjection; +global using Microsoft.Extensions.DependencyInjection.Extensions; diff --git a/src/Isolation/Masa.Contrib.Isolation.MultiTenant/ConvertProvider.cs b/src/Isolation/Masa.Contrib.Isolation.MultiTenant/ConvertProvider.cs new file mode 100644 index 000000000..1594b596f --- /dev/null +++ b/src/Isolation/Masa.Contrib.Isolation.MultiTenant/ConvertProvider.cs @@ -0,0 +1,18 @@ +namespace Masa.Contrib.Isolation.MultiTenant; + +public class ConvertProvider : IConvertProvider +{ + public object ChangeType(string value, Type conversionType) + { + object result; + if (conversionType == typeof(Guid)) + { + result = Guid.Parse(value); + } + else + { + result = Convert.ChangeType(value, conversionType); + } + return result; + } +} diff --git a/src/Isolation/Masa.Contrib.Isolation.MultiTenant/IsolationBuilderExtensions.cs b/src/Isolation/Masa.Contrib.Isolation.MultiTenant/IsolationBuilderExtensions.cs new file mode 100644 index 000000000..ef6af4290 --- /dev/null +++ b/src/Isolation/Masa.Contrib.Isolation.MultiTenant/IsolationBuilderExtensions.cs @@ -0,0 +1,16 @@ +namespace Masa.Contrib.Isolation.MultiTenant; + +public static class IsolationBuilderExtensions +{ + public static IIsolationBuilder UseMultiTenant(this IIsolationBuilder isolationBuilder) => isolationBuilder.UseMultiTenant(); + + public static IIsolationBuilder UseMultiTenant(this IIsolationBuilder isolationBuilder) where TKey : IComparable + { + isolationBuilder.Services.TryAddSingleton(); + isolationBuilder.Services.TryAddEnumerable(new ServiceDescriptor(typeof(ISaveChangesFilter), typeof(TenantSaveChangesFilter), ServiceLifetime.Scoped)); + isolationBuilder.Services.TryAddScoped(); + isolationBuilder.Services.TryAddScoped(typeof(ITenantContext), serviceProvider => serviceProvider.GetRequiredService()); + isolationBuilder.Services.TryAddScoped(typeof(ITenantSetter), serviceProvider => serviceProvider.GetRequiredService()); + return isolationBuilder; + } +} diff --git a/src/Isolation/Masa.Contrib.Isolation.MultiTenant/Masa.Contrib.Isolation.MultiTenant.csproj b/src/Isolation/Masa.Contrib.Isolation.MultiTenant/Masa.Contrib.Isolation.MultiTenant.csproj new file mode 100644 index 000000000..8f2f07b48 --- /dev/null +++ b/src/Isolation/Masa.Contrib.Isolation.MultiTenant/Masa.Contrib.Isolation.MultiTenant.csproj @@ -0,0 +1,17 @@ + + + + net6.0 + enable + enable + + + + + + + + + + + diff --git a/src/Isolation/Masa.Contrib.Isolation.MultiTenant/README.md b/src/Isolation/Masa.Contrib.Isolation.MultiTenant/README.md new file mode 100644 index 000000000..9e1092506 --- /dev/null +++ b/src/Isolation/Masa.Contrib.Isolation.MultiTenant/README.md @@ -0,0 +1,99 @@ +[中](README.zh-CN.md) | EN + +## Masa.Contrib.Isolation.MultiTenant + +Example: + +```C# +Install-Package Masa.Contrib.Isolation.UoW.EF +Install-Package Masa.Contrib.Isolation.MultiTenant +Install-Package Masa.Utils.Data.EntityFrameworkCore.SqlServer +``` + +1. 配置appsettings.json +``` appsettings.json +{ + "ConnectionStrings": { + "DefaultConnection": "server=localhost;uid=sa;pwd=P@ssw0rd;database=identity;", + "Isolations": [ + { + "TenantId": "00000000-0000-0000-0000-000000000002", + "ConnectionString": "server=localhost,1674;uid=sa;pwd=P@ssw0rd;database=identity;" + }, + { + "TenantId": "00000000-0000-0000-0000-000000000003", + "ConnectionString": "server=localhost,1672;uid=sa;pwd=P@ssw0rd;database=identity;" + } + ] + } +} +``` + +* 1.1 When the current tenant is 00000000-0000-0000-0000-000000000002: database address: server=localhost,1674;uid=sa;pwd=P@ssw0rd;database=identity; +* 1.2 When the current tenant is 00000000-0000-0000-0000-000000000003: database address: server=localhost,1672;uid=sa;pwd=P@ssw0rd;database=identity; +* 1.3 Other tenants: database address: server=localhost;uid=sa;pwd=P@ssw0rd;database=identity; + +2. Using Isolation.UoW.EF +```` C# +builder.Services.AddEventBus(eventBusBuilder => +{ + eventBusBuilder.UseIsolationUoW( + dbOptions => dbOptions.UseSqlServer(), + isolationBuilder => isolationBuilder.UseMultiTenant());// Use tenant isolation +}); +```` + +3. DbContext needs to inherit IsolationDbContext + +```` C# +public class CustomDbContext : IsolationDbContext +{ + public CustomDbContext(MasaDbContextOptions options) : base(options) + { + } +} +```` + +4. The class corresponding to the isolated table needs to implement IMultiTenant + +You can also choose not to implement IMultiTenant when using physical isolation + +> The tenant id type can be specified by yourself, the default Guid type +> * For example: Implement IMultiTenant to implement IMultiTenant, UseMultiTenant() to UseMultiTenant() + +##### Summarize + +* How to resolve the tenant in the controller or MinimalAPI? + * The tenant provides 6 parsers by default, the execution order is: HttpContextItemTenantParserProvider, QueryStringTenantParserProvider, FormTenantParserProvider, RouteTenantParserProvider, HeaderTenantParserProvider, CookieTenantParserProvider (tenant parameter default: __tenant) + * HttpContextItemTenantParserProvider: Obtain tenant information through the Items property of the requested HttpContext + * QueryStringTenantParserProvider: Get tenant information through the requested QueryString + * https://github.com/masastack?__tenant=1 (tenant id is 1) + * FormTenantParserProvider: Get tenant information through the Form form + * RouteTenantParserProvider: Get tenant information through routing + * HeaderTenantParserProvider: Get tenant information through request headers + * CookieTenantParserProvider: Get tenant information through cookies +* If the resolver fails to resolve the tenant, what is the last database used? + * If the resolution of the tenant fails, return the DefaultConnection directly +* How to change the default tenant parameter name + +```` C# +builder.Services.AddEventBus(eventBusBuilder => +{ + eventBusBuilder.UseIsolationUoW( + dbOptions => dbOptions.UseSqlServer(), + isolationBuilder => isolationBuilder.SetTenantKey("tenant").UseMultiTenant());// Use tenant isolation +}); +```` +* The default parser is not easy to use, want to change the default parser? + +```` C# +builder.Services.AddEventBus(eventBusBuilder => +{ + eventBusBuilder.UseIsolationUoW( + dbOptions => dbOptions.UseSqlServer(), + isolationBuilder => isolationBuilder.SetTenantParsers(new List() + { + new QueryStringTenantParserProvider() // only use QueryStringTenantParserProvider, other parsers are removed + }).UseMultiTenant());// Use tenant isolation +}); +```` diff --git a/src/Isolation/Masa.Contrib.Isolation.MultiTenant/README.zh-CN.md b/src/Isolation/Masa.Contrib.Isolation.MultiTenant/README.zh-CN.md new file mode 100644 index 000000000..44f0c5971 --- /dev/null +++ b/src/Isolation/Masa.Contrib.Isolation.MultiTenant/README.zh-CN.md @@ -0,0 +1,100 @@ +中 | [EN](README.md) + +## Masa.Contrib.Isolation.MultiTenant + +用例: + +```C# +Install-Package Masa.Contrib.Isolation.UoW.EF +Install-Package Masa.Contrib.Isolation.MultiTenant +Install-Package Masa.Utils.Data.EntityFrameworkCore.SqlServer +``` + +1. 配置appsettings.json +``` appsettings.json +{ + "ConnectionStrings": { + "DefaultConnection": "server=localhost;uid=sa;pwd=P@ssw0rd;database=identity;", + "Isolations": [ + { + "TenantId": "00000000-0000-0000-0000-000000000002", + "ConnectionString": "server=localhost,1674;uid=sa;pwd=P@ssw0rd;database=identity;" + }, + { + "TenantId": "00000000-0000-0000-0000-000000000003", + "ConnectionString": "server=localhost,1672;uid=sa;pwd=P@ssw0rd;database=identity;" + } + ] + } +} +``` + +* 1.1 当前租户为00000000-0000-0000-0000-000000000002时:数据库地址:server=localhost,1674;uid=sa;pwd=P@ssw0rd;database=identity; +* 1.2 当前租户为00000000-0000-0000-0000-000000000003时:数据库地址:server=localhost,1672;uid=sa;pwd=P@ssw0rd;database=identity; +* 1.3 其他租户:数据库地址:server=localhost;uid=sa;pwd=P@ssw0rd;database=identity; + +2. 使用Isolation.UoW.EF +``` C# +builder.Services.AddEventBus(eventBusBuilder => +{ + eventBusBuilder.UseIsolationUoW( + dbOptions => dbOptions.UseSqlServer(), + isolationBuilder => isolationBuilder.UseMultiTenant());// 使用租户隔离 +}); +``` + +3. DbContext需要继承IsolationDbContext + +``` C# +public class CustomDbContext : IsolationDbContext +{ + public CustomDbContext(MasaDbContextOptions options) : base(options) + { + } +} +``` + +4. 隔离的表对应的类需要实现IMultiTenant + +采用物理隔离时也可以选择不实现IMultiTenant + +> 租户id类型支持自行指定,默认Guid类型 +> * 如:实现IMultiTenant改为实现IMultiTenant,UseMultiTenant()改为UseMultiTenant() + +##### 总结 + +* 控制器或MinimalAPI中租户如何解析? + * 租户默认提供了6个解析器,执行顺序分别为:HttpContextItemTenantParserProvider、QueryStringTenantParserProvider、FormTenantParserProvider、RouteTenantParserProvider、HeaderTenantParserProvider、CookieTenantParserProvider (租户参数默认:__tenant) + * HttpContextItemTenantParserProvider: 通过请求的HttpContext的Items属性获取租户信息 + * QueryStringTenantParserProvider: 通过请求的QueryString获取租户信息 + * https://github.com/masastack?__tenant=1 (租户id为1) + * FormTenantParserProvider: 通过Form表单获取租户信息 + * RouteTenantParserProvider: 通过路由获取租户信息 + * HeaderTenantParserProvider: 通过请求头获取租户信息 + * CookieTenantParserProvider: 通过Cookie获取租户信息 +* 如果解析器解析租户失败,那最后使用的数据库是? + * 若解析租户失败,则直接返回DefaultConnection +* 如何更改默认租户参数名 + +``` C# +builder.Services.AddEventBus(eventBusBuilder => +{ + eventBusBuilder.UseIsolationUoW( + dbOptions => dbOptions.UseSqlServer(), + isolationBuilder => isolationBuilder.SetTenantKey("tenant").UseMultiTenant());// 使用租户隔离 +}); +``` +* 默认解析器不好用,希望更改默认解析器? + +``` C# +builder.Services.AddEventBus(eventBusBuilder => +{ + eventBusBuilder.UseIsolationUoW( + dbOptions => dbOptions.UseSqlServer(), + isolationBuilder => isolationBuilder.SetTenantParsers(new List() + { + new QueryStringTenantParserProvider() // 只使用QueryStringTenantParserProvider, 其它解析器移除掉 + }).UseMultiTenant());// 使用租户隔离 +}); +``` + diff --git a/src/Isolation/Masa.Contrib.Isolation.MultiTenant/TenantContext.cs b/src/Isolation/Masa.Contrib.Isolation.MultiTenant/TenantContext.cs new file mode 100644 index 000000000..595e64289 --- /dev/null +++ b/src/Isolation/Masa.Contrib.Isolation.MultiTenant/TenantContext.cs @@ -0,0 +1,8 @@ +namespace Masa.Contrib.Isolation.MultiTenant; + +public class TenantContext : ITenantContext, ITenantSetter +{ + public Tenant? CurrentTenant { get; private set; } + + public void SetTenant(Tenant? tenant) => CurrentTenant = tenant; +} diff --git a/src/Isolation/Masa.Contrib.Isolation.MultiTenant/TenantSaveChangesFilter.cs b/src/Isolation/Masa.Contrib.Isolation.MultiTenant/TenantSaveChangesFilter.cs new file mode 100644 index 000000000..c99d996db --- /dev/null +++ b/src/Isolation/Masa.Contrib.Isolation.MultiTenant/TenantSaveChangesFilter.cs @@ -0,0 +1,33 @@ +namespace Masa.Contrib.Isolation.MultiTenant; + +public class TenantSaveChangesFilter : ISaveChangesFilter where TKey : IComparable +{ + private readonly ITenantContext _tenantContext; + private readonly IConvertProvider _convertProvider; + + public TenantSaveChangesFilter(ITenantContext tenantContext, IConvertProvider convertProvider) + { + _tenantContext = tenantContext; + _convertProvider = convertProvider; + } + + public void OnExecuting(ChangeTracker changeTracker) + { + changeTracker.DetectChanges(); + foreach (var entity in changeTracker.Entries().Where(entry => entry.State == EntityState.Added)) + { + if (entity.Entity is IMultiTenant) + { + if (_tenantContext.CurrentTenant != null && !string.IsNullOrEmpty(_tenantContext.CurrentTenant.Id)) + { + object tenantId = _convertProvider.ChangeType(_tenantContext.CurrentTenant.Id, typeof(TKey)); + entity.CurrentValues[nameof(IMultiTenant.TenantId)] = tenantId; + } + else + { + entity.CurrentValues[nameof(IMultiTenant.TenantId)] = default(TKey); + } + } + } + } +} diff --git a/src/Isolation/Masa.Contrib.Isolation.MultiTenant/_Imports.cs b/src/Isolation/Masa.Contrib.Isolation.MultiTenant/_Imports.cs new file mode 100644 index 000000000..7d4068925 --- /dev/null +++ b/src/Isolation/Masa.Contrib.Isolation.MultiTenant/_Imports.cs @@ -0,0 +1,8 @@ +global using Masa.BuildingBlocks.Isolation; +global using Masa.BuildingBlocks.Isolation.MultiTenant; +global using Masa.Utils.Data.EntityFrameworkCore.Filters; +global using Microsoft.EntityFrameworkCore; +global using Microsoft.EntityFrameworkCore.ChangeTracking; +global using Microsoft.Extensions.DependencyInjection; +global using Microsoft.Extensions.DependencyInjection.Extensions; +global using System.Linq; diff --git a/src/Isolation/Masa.Contrib.Isolation.UoW.EF/DefaultConnectionStringProvider.cs b/src/Isolation/Masa.Contrib.Isolation.UoW.EF/DefaultConnectionStringProvider.cs new file mode 100644 index 000000000..d130c3177 --- /dev/null +++ b/src/Isolation/Masa.Contrib.Isolation.UoW.EF/DefaultConnectionStringProvider.cs @@ -0,0 +1,87 @@ +namespace Masa.Contrib.Isolation.UoW.EF; + +public class DefaultConnectionStringProvider : IConnectionStringProvider +{ + private readonly IUnitOfWorkAccessor _unitOfWorkAccessor; + private readonly IOptionsSnapshot _options; + private readonly IEnvironmentContext? _environmentContext; + private readonly ITenantContext? _tenantContext; + private readonly ILogger? _logger; + + public DefaultConnectionStringProvider( + IUnitOfWorkAccessor unitOfWorkAccessor, + IOptionsSnapshot options, + IEnvironmentContext? environmentContext = null, + ITenantContext? tenantContext = null, + ILogger? logger = null) + { + _unitOfWorkAccessor = unitOfWorkAccessor; + _options = options; + _environmentContext = environmentContext; + _tenantContext = tenantContext; + _logger = logger; + } + + public Task GetConnectionStringAsync() => Task.FromResult(GetConnectionString()); + + public string GetConnectionString() + { + if (_unitOfWorkAccessor.CurrentDbContextOptions != null) + return _unitOfWorkAccessor.CurrentDbContextOptions.ConnectionString; + + Expression> condition = option => true; + + if (_tenantContext != null) + { + if (_tenantContext.CurrentTenant == null) + { + _logger?.LogError($"Tenant resolution failed, the currently used ConnectionString is [{nameof(_options.Value.DefaultConnection)}]"); + return SetConnectionString(); + } + + condition = condition.And(option => option.TenantId == "*" || (_tenantContext.CurrentTenant!=null && _tenantContext.CurrentTenant.Id.Equals(option.TenantId, StringComparison.CurrentCultureIgnoreCase))); + } + + if (_environmentContext != null) + { + if (string.IsNullOrEmpty(_environmentContext.CurrentEnvironment)) + { + _logger?.LogError($"Environment resolution failed, the currently used ConnectionString is [{nameof(_options.Value.DefaultConnection)}]"); + return SetConnectionString(); + } + + condition = condition.And(option => option.Environment == "*" || option.Environment.Equals(_environmentContext.CurrentEnvironment, StringComparison.CurrentCultureIgnoreCase)); + } + + string connectionString; + var list = _options.Value.Isolations.Where(condition.Compile()).ToList(); + if (list.Count >= 1) + { + connectionString = list.OrderByDescending(option=>option.Score).Select(option => option.ConnectionString).FirstOrDefault()!; + if (list.Count > 1) + _logger?.LogInformation($"{GetMessage()}, Matches multiple available database link strings, the currently used ConnectionString is [{connectionString}]"); + } + else + { + connectionString = _options.Value.DefaultConnection; + _logger?.LogDebug($"{GetMessage()}, the currently used ConnectionString is [{nameof(_options.Value.DefaultConnection)}]"); + } + return SetConnectionString(connectionString); + } + + private string SetConnectionString(string? connectionString = null) + { + _unitOfWorkAccessor.CurrentDbContextOptions = new MasaDbContextConfigurationOptions(connectionString ?? _options.Value.DefaultConnection); + return _unitOfWorkAccessor.CurrentDbContextOptions.ConnectionString; + } + + private string GetMessage() + { + List messages = new List(); + if (_environmentContext != null) + messages.Add($"Environment: [{_environmentContext.CurrentEnvironment ?? ""}]"); + if (_tenantContext != null) + messages.Add($"Tenant: [{_tenantContext.CurrentTenant?.Id ?? ""}]"); + return string.Join(", ", messages); + } +} diff --git a/src/Isolation/Masa.Contrib.Isolation.UoW.EF/DispatcherOptionsExtensions.cs b/src/Isolation/Masa.Contrib.Isolation.UoW.EF/DispatcherOptionsExtensions.cs new file mode 100644 index 000000000..fb9eb53ac --- /dev/null +++ b/src/Isolation/Masa.Contrib.Isolation.UoW.EF/DispatcherOptionsExtensions.cs @@ -0,0 +1,93 @@ +namespace Masa.Contrib.Isolation.UoW.EF; + +public static class DispatcherOptionsExtensions +{ + public static IEventBusBuilder UseIsolationUoW( + this IEventBusBuilder eventBusBuilder, + Action? optionsBuilder, + Action isolationBuilder, + bool disableRollbackOnFailure = false, + bool useTransaction = true) + where TDbContext : MasaDbContext + { + eventBusBuilder.Services.UseIsolationUoW(nameof(eventBusBuilder.Services), isolationBuilder); + return eventBusBuilder.UseUoW(optionsBuilder, disableRollbackOnFailure, useTransaction); + } + + public static IDispatcherOptions UseIsolationUoW( + this IDispatcherOptions options, + Action? optionsBuilder, + Action isolationBuilder, + bool disableRollbackOnFailure = false, + bool useTransaction = true) + where TDbContext : MasaDbContext + { + options.Services.UseIsolationUoW(nameof(options.Services), isolationBuilder); + return options.UseUoW(optionsBuilder, disableRollbackOnFailure, useTransaction); + } + + private static IServiceCollection UseIsolationUoW( + this IServiceCollection services, + string paramName, + Action isolationBuilder) + { + ArgumentNullException.ThrowIfNull(services, paramName); + ArgumentNullException.ThrowIfNull(isolationBuilder); + + if (services.Any(service => service.ImplementationType == typeof(IsolationUoWProvider))) + return services; + + services.AddSingleton(); + + IsolationBuilder builder = new IsolationBuilder(services); + isolationBuilder.Invoke(builder); + + if (services.Count(service => service.ServiceType == typeof(ITenantContext) || service.ServiceType == typeof(IEnvironmentContext)) < 1) + throw new NotSupportedException("Tenant isolation and environment isolation use at least one"); + + services.Configure(option => + { + option.TenantKey = builder.TenantKey; + option.EnvironmentKey = builder.EnvironmentKey; + }); + + services.AddHttpContextAccessor(); + + if (services.Any(service => service.ServiceType == typeof(ITenantContext))) + services.AddScoped(serviceProvider => new MultiTenantMiddleware(serviceProvider, builder.TenantParsers)); + + if (services.Any(service => service.ServiceType == typeof(IEnvironmentContext))) + services.AddScoped(serviceProvider => new MultiEnvironmentMiddleware(serviceProvider, builder.EnvironmentParsers)); + + services.AddTransient(typeof(IMiddleware<>), typeof(IsolationMiddleware<>)); + services.TryAddSingleton(); + services.TryAddConfigure(Const.DEFAULT_SECTION); + services.TryAddScoped(typeof(IConnectionStringProvider), typeof(DefaultConnectionStringProvider)); + return services; + } + + private static IServiceCollection TryAddConfigure( + this IServiceCollection services, + string sectionName) + where TOptions : class + { + IConfiguration? configuration = services.BuildServiceProvider().GetService(); + if (configuration == null) + return services; + + string name = Options.DefaultName; + services.AddOptions(); + var configurationSection = configuration.GetSection(sectionName); + services.TryAddSingleton>( + new ConfigurationChangeTokenSource(name, configurationSection)); + services.TryAddSingleton>(new NamedConfigureFromConfigurationOptions(name, + configurationSection, _ => + { + })); + return services; + } + + private class IsolationUoWProvider + { + } +} diff --git a/src/Isolation/Masa.Contrib.Isolation.UoW.EF/Internal/Const.cs b/src/Isolation/Masa.Contrib.Isolation.UoW.EF/Internal/Const.cs new file mode 100644 index 000000000..7e66d8486 --- /dev/null +++ b/src/Isolation/Masa.Contrib.Isolation.UoW.EF/Internal/Const.cs @@ -0,0 +1,6 @@ +namespace Masa.Contrib.Isolation.UoW.EF.Internal; + +internal class Const +{ + public const string DEFAULT_SECTION = "ConnectionStrings"; +} diff --git a/src/Isolation/Masa.Contrib.Isolation.UoW.EF/Internal/TypeExtensions.cs b/src/Isolation/Masa.Contrib.Isolation.UoW.EF/Internal/TypeExtensions.cs new file mode 100644 index 000000000..b09229d28 --- /dev/null +++ b/src/Isolation/Masa.Contrib.Isolation.UoW.EF/Internal/TypeExtensions.cs @@ -0,0 +1,10 @@ +namespace Masa.Contrib.Isolation.UoW.EF.Internal; + +internal static class TypeExtensions +{ + static bool IsConcrete(this Type type) => !type.GetTypeInfo().IsAbstract && !type.GetTypeInfo().IsInterface; + + public static bool IsGenericInterfaceAssignableFrom(this Type genericType, Type type) => + type.IsConcrete() && + type.GetInterfaces().Any(t => t.GetTypeInfo().IsGenericType && t.GetGenericTypeDefinition() == genericType); +} diff --git a/src/Isolation/Masa.Contrib.Isolation.UoW.EF/IsolationBuilder.cs b/src/Isolation/Masa.Contrib.Isolation.UoW.EF/IsolationBuilder.cs new file mode 100644 index 000000000..26eac15d7 --- /dev/null +++ b/src/Isolation/Masa.Contrib.Isolation.UoW.EF/IsolationBuilder.cs @@ -0,0 +1,62 @@ +namespace Masa.Contrib.Isolation.UoW.EF; + +public class IsolationBuilder : IIsolationBuilder +{ + public IServiceCollection Services { get; } + + public string EnvironmentKey { get; private set; } + + public string TenantKey { get; private set;} + + private List _tenantParsers; + + public IReadOnlyCollection TenantParsers => _tenantParsers; + + private List _environmentParsers; + + public IReadOnlyCollection EnvironmentParsers => _environmentParsers; + + public IsolationBuilder(IServiceCollection services) + { + Services = services; + EnvironmentKey = "ASPNETCORE_ENVIRONMENT"; + TenantKey = "__tenant"; + _tenantParsers = new List() + { + new HttpContextItemTenantParserProvider(), + new QueryStringTenantParserProvider(), + new FormTenantParserProvider(), + new RouteTenantParserProvider(), + new HeaderTenantParserProvider(), + new CookieTenantParserProvider() + }; + _environmentParsers = new List() + { + new EnvironmentVariablesParserProvider() + }; + } + + public IsolationBuilder SetEnvironmentKey(string environmentKey) + { + EnvironmentKey = environmentKey; + return this; + } + + public IsolationBuilder SetTenantKey(string tenantKey) + { + TenantKey = tenantKey; + return this; + } + + public IsolationBuilder SetTenantParsers(List tenantParserProviders) + { + _tenantParsers = tenantParserProviders; + return this; + } + + public IsolationBuilder SetEnvironmentParsers(List environmentParserProviders) + { + _environmentParsers = environmentParserProviders; + return this; + } +} diff --git a/src/Isolation/Masa.Contrib.Isolation.UoW.EF/IsolationBuilderExtensions.cs b/src/Isolation/Masa.Contrib.Isolation.UoW.EF/IsolationBuilderExtensions.cs new file mode 100644 index 000000000..5d48710d4 --- /dev/null +++ b/src/Isolation/Masa.Contrib.Isolation.UoW.EF/IsolationBuilderExtensions.cs @@ -0,0 +1,10 @@ +namespace Masa.Contrib.Isolation.UoW.EF; + +public static class IsolationBuilderExtensions +{ + public static TApplicationBuilder UseIsolation(this TApplicationBuilder app) where TApplicationBuilder : IApplicationBuilder + { + app.UseMiddleware(); + return app; + } +} diff --git a/src/Isolation/Masa.Contrib.Isolation.UoW.EF/IsolationDbContext.cs b/src/Isolation/Masa.Contrib.Isolation.UoW.EF/IsolationDbContext.cs new file mode 100644 index 000000000..595c1e076 --- /dev/null +++ b/src/Isolation/Masa.Contrib.Isolation.UoW.EF/IsolationDbContext.cs @@ -0,0 +1,57 @@ +namespace Masa.Contrib.Isolation.UoW.EF; + +public abstract class IsolationDbContext : IsolationDbContext +{ + protected IsolationDbContext(MasaDbContextOptions options) : base(options) + { + } +} + +/// +/// DbContext providing isolation +/// +/// tenant id type +public abstract class IsolationDbContext : MasaDbContext + where TKey : IComparable +{ + private readonly IEnvironmentContext? _environmentContext; + private readonly ITenantContext? _tenantContext; + + public IsolationDbContext(MasaDbContextOptions options) : base(options) + { + _environmentContext = options.ServiceProvider.GetService(); + _tenantContext = options.ServiceProvider.GetService(); + } + + protected override Expression>? CreateFilterExpression() + where TEntity : class + { + Expression>? expression = null; + + if (typeof(IMultiTenant<>).IsGenericInterfaceAssignableFrom(typeof(TEntity)) && _tenantContext != null) + { + Expression> tenantFilter = entity => !IsTenantFilterEnabled || + Microsoft.EntityFrameworkCore.EF.Property(entity, nameof(IMultiTenant.TenantId)) + .Equals(_tenantContext.CurrentTenant != null ? _tenantContext.CurrentTenant.Id : default(TKey)); + expression = tenantFilter.And(expression != null, expression); + } + + if (typeof(IMultiEnvironment).IsAssignableFrom(typeof(TEntity)) && _environmentContext != null) + { + Expression> envFilter = entity => !IsEnvironmentFilterEnabled || + Microsoft.EntityFrameworkCore.EF.Property(entity, nameof(IMultiEnvironment.Environment)) + .Equals(_environmentContext != null ? _environmentContext.CurrentEnvironment : default); + expression = envFilter.And(expression != null, expression); + } + + var secondExpression = base.CreateFilterExpression(); + if (secondExpression != null) + expression = secondExpression.And(expression != null, expression); + + return expression; + } + + protected virtual bool IsEnvironmentFilterEnabled => DataFilter?.IsEnabled() ?? false; + + protected virtual bool IsTenantFilterEnabled => DataFilter?.IsEnabled>() ?? false; +} diff --git a/src/Isolation/Masa.Contrib.Isolation.UoW.EF/IsolationDbContextProvider.cs b/src/Isolation/Masa.Contrib.Isolation.UoW.EF/IsolationDbContextProvider.cs new file mode 100644 index 000000000..0e44917ca --- /dev/null +++ b/src/Isolation/Masa.Contrib.Isolation.UoW.EF/IsolationDbContextProvider.cs @@ -0,0 +1,20 @@ +namespace Masa.Contrib.Isolation.UoW.EF; + +public class IsolationDbContextProvider : BaseDbConnectionStringProvider +{ + private readonly IOptionsMonitor _options; + + public IsolationDbContextProvider(IOptionsMonitor options) => _options = options; + + protected override List GetDbContextOptionsList() + { + var connectionStrings = _options.CurrentValue.Isolations + .Select(connectionString => connectionString.ConnectionString) + .Distinct() + .ToList(); + if (!connectionStrings.Contains(_options.CurrentValue.DefaultConnection)) + connectionStrings.Add(_options.CurrentValue.DefaultConnection); + + return connectionStrings.Select(connectionString => new MasaDbContextConfigurationOptions(connectionString)).ToList(); + } +} diff --git a/src/Isolation/Masa.Contrib.Isolation.UoW.EF/Masa.Contrib.Isolation.UoW.EF.csproj b/src/Isolation/Masa.Contrib.Isolation.UoW.EF/Masa.Contrib.Isolation.UoW.EF.csproj new file mode 100644 index 000000000..59e598ccf --- /dev/null +++ b/src/Isolation/Masa.Contrib.Isolation.UoW.EF/Masa.Contrib.Isolation.UoW.EF.csproj @@ -0,0 +1,21 @@ + + + + net6.0 + enable + enable + + + + + + + + + + + + + + + diff --git a/src/Isolation/Masa.Contrib.Isolation.UoW.EF/Middleware/IIsolationMiddleware.cs b/src/Isolation/Masa.Contrib.Isolation.UoW.EF/Middleware/IIsolationMiddleware.cs new file mode 100644 index 000000000..fc2398d39 --- /dev/null +++ b/src/Isolation/Masa.Contrib.Isolation.UoW.EF/Middleware/IIsolationMiddleware.cs @@ -0,0 +1,6 @@ +namespace Masa.Contrib.Isolation.UoW.EF.Middleware; + +public interface IIsolationMiddleware +{ + Task HandleAsync(); +} diff --git a/src/Isolation/Masa.Contrib.Isolation.UoW.EF/Middleware/IsolationMiddleware.cs b/src/Isolation/Masa.Contrib.Isolation.UoW.EF/Middleware/IsolationMiddleware.cs new file mode 100644 index 000000000..8e367c50e --- /dev/null +++ b/src/Isolation/Masa.Contrib.Isolation.UoW.EF/Middleware/IsolationMiddleware.cs @@ -0,0 +1,41 @@ +namespace Masa.Contrib.Isolation.UoW.EF.Middleware; + +public class IsolationMiddleware : IMiddleware where TEvent : IEvent +{ + private readonly IEnumerable _middlewares; + + public IsolationMiddleware(IEnumerable middlewares) + { + _middlewares = middlewares; + } + + public async Task HandleAsync(TEvent @event, EventHandlerDelegate next) + { + foreach (var middleware in _middlewares) + { + await middleware.HandleAsync(); + } + + await next(); + } +} + +public class IsolationMiddleware +{ + private readonly RequestDelegate _next; + + public IsolationMiddleware(RequestDelegate next) + { + _next = next; + } + + public async Task InvokeAsync(HttpContext httpContext, IEnumerable middlewares) + { + foreach (var middleware in middlewares) + { + await middleware.HandleAsync(); + } + + await _next(httpContext); + } +} diff --git a/src/Isolation/Masa.Contrib.Isolation.UoW.EF/Middleware/MultiEnvironmentMiddleware.cs b/src/Isolation/Masa.Contrib.Isolation.UoW.EF/Middleware/MultiEnvironmentMiddleware.cs new file mode 100644 index 000000000..95aab59ea --- /dev/null +++ b/src/Isolation/Masa.Contrib.Isolation.UoW.EF/Middleware/MultiEnvironmentMiddleware.cs @@ -0,0 +1,44 @@ +namespace Masa.Contrib.Isolation.UoW.EF.Middleware; + +public class MultiEnvironmentMiddleware : IIsolationMiddleware +{ + private readonly IServiceProvider _serviceProvider; + private readonly ILogger? _logger; + private readonly IEnvironmentContext _environmentContext; + private readonly IEnumerable _environmentParserProviders; + private bool _handled; + + public MultiEnvironmentMiddleware(IServiceProvider serviceProvider, IEnumerable environmentParserProviders) + { + _serviceProvider = serviceProvider; + _logger = _serviceProvider.GetService>(); + _environmentContext = _serviceProvider.GetRequiredService(); + _environmentParserProviders = environmentParserProviders; + } + + public async Task HandleAsync() + { + if(_handled) + return; + + if (!string.IsNullOrEmpty(_environmentContext.CurrentEnvironment)) + { + _logger?.LogDebug($"The environment is successfully resolved, and the resolver is: empty"); + return; + } + + List parsers = new(); + foreach (var environmentParserProvider in _environmentParserProviders) + { + parsers.Add(environmentParserProvider.Name); + if (await environmentParserProvider.ResolveAsync(_serviceProvider)) + { + _logger?.LogDebug($"The environment is successfully resolved, and the resolver is: {string.Join("、 ",parsers)}"); + _handled = true; + return; + } + } + _logger?.LogDebug($"Failed to resolve environment, and the resolver is: {string.Join("、 ",parsers)}"); + _handled = true; + } +} diff --git a/src/Isolation/Masa.Contrib.Isolation.UoW.EF/Middleware/MultiTenantMiddleware.cs b/src/Isolation/Masa.Contrib.Isolation.UoW.EF/Middleware/MultiTenantMiddleware.cs new file mode 100644 index 000000000..737664ca8 --- /dev/null +++ b/src/Isolation/Masa.Contrib.Isolation.UoW.EF/Middleware/MultiTenantMiddleware.cs @@ -0,0 +1,43 @@ +namespace Masa.Contrib.Isolation.UoW.EF.Middleware; + +public class MultiTenantMiddleware : IIsolationMiddleware +{ + private readonly IServiceProvider _serviceProvider; + private readonly ILogger? _logger; + private readonly ITenantContext _tenantContext; + private readonly IEnumerable _tenantParserProviders; + private bool _handled; + public MultiTenantMiddleware(IServiceProvider serviceProvider, IEnumerable tenantParserProviders) + { + _serviceProvider = serviceProvider; + _logger = _serviceProvider.GetService>(); + _tenantContext = _serviceProvider.GetRequiredService(); + _tenantParserProviders = tenantParserProviders; + } + + public async Task HandleAsync() + { + if(_handled) + return; + + if (_tenantContext.CurrentTenant != null && !string.IsNullOrEmpty(_tenantContext.CurrentTenant.Id)) + { + _logger?.LogDebug($"The tenant is successfully resolved, and the resolver is: empty"); + return; + } + + List parsers = new(); + foreach (var tenantParserProvider in _tenantParserProviders) + { + parsers.Add(tenantParserProvider.Name); + if (await tenantParserProvider.ResolveAsync(_serviceProvider)) + { + _logger?.LogDebug($"The tenant is successfully resolved, and the resolver is: {string.Join("、 ", parsers)}"); + _handled = true; + return; + } + } + _logger?.LogDebug($"Failed to resolve tenant, and the resolver is: {string.Join("、 ", parsers)}"); + _handled = true; + } +} diff --git a/src/Isolation/Masa.Contrib.Isolation.UoW.EF/Parser/Environment/EnvironmentVariablesParserProvider.cs b/src/Isolation/Masa.Contrib.Isolation.UoW.EF/Parser/Environment/EnvironmentVariablesParserProvider.cs new file mode 100644 index 000000000..066c7ab78 --- /dev/null +++ b/src/Isolation/Masa.Contrib.Isolation.UoW.EF/Parser/Environment/EnvironmentVariablesParserProvider.cs @@ -0,0 +1,19 @@ +namespace Masa.Contrib.Isolation.UoW.EF.Parser.Environment; + +public class EnvironmentVariablesParserProvider : IEnvironmentParserProvider +{ + public string Name { get; } = "EnvironmentVariables"; + + public Task ResolveAsync(IServiceProvider serviceProvider) + { + var environmentSetter = serviceProvider.GetRequiredService(); + var options = serviceProvider.GetRequiredService>(); + string? environment = System.Environment.GetEnvironmentVariable(options.Value.EnvironmentKey); + if (environment != null && !string.IsNullOrEmpty(environment)) + { + environmentSetter.SetEnvironment(environment); + return Task.FromResult(true); + } + return Task.FromResult(false); + } +} diff --git a/src/Isolation/Masa.Contrib.Isolation.UoW.EF/Parser/MultiTenant/CookieTenantParserProvider.cs b/src/Isolation/Masa.Contrib.Isolation.UoW.EF/Parser/MultiTenant/CookieTenantParserProvider.cs new file mode 100644 index 000000000..caa46a285 --- /dev/null +++ b/src/Isolation/Masa.Contrib.Isolation.UoW.EF/Parser/MultiTenant/CookieTenantParserProvider.cs @@ -0,0 +1,24 @@ +namespace Masa.Contrib.Isolation.UoW.EF.Parser.MultiTenant; + +public class CookieTenantParserProvider : ITenantParserProvider +{ + public string Name => "Cookie"; + + public Task ResolveAsync(IServiceProvider serviceProvider) + { + var httpContext = serviceProvider.GetService()?.HttpContext; + var tenantSetter = serviceProvider.GetRequiredService(); + var options = serviceProvider.GetRequiredService>(); + if (httpContext?.Request.Cookies.ContainsKey(options.Value.TenantKey) ?? false) + { + var tenantId = httpContext.Request.Cookies[options.Value.TenantKey]!; + if (!string.IsNullOrEmpty(tenantId)) + { + tenantSetter.SetTenant(new Tenant(tenantId)); + return Task.FromResult(true); + } + } + + return Task.FromResult(false); + } +} diff --git a/src/Isolation/Masa.Contrib.Isolation.UoW.EF/Parser/MultiTenant/FormTenantParserProvider.cs b/src/Isolation/Masa.Contrib.Isolation.UoW.EF/Parser/MultiTenant/FormTenantParserProvider.cs new file mode 100644 index 000000000..1858a7208 --- /dev/null +++ b/src/Isolation/Masa.Contrib.Isolation.UoW.EF/Parser/MultiTenant/FormTenantParserProvider.cs @@ -0,0 +1,26 @@ +namespace Masa.Contrib.Isolation.UoW.EF.Parser.MultiTenant; + +public class FormTenantParserProvider : ITenantParserProvider +{ + public string Name => "Form"; + + public Task ResolveAsync(IServiceProvider serviceProvider) + { + var httpContext = serviceProvider.GetService()?.HttpContext; + var tenantSetter = serviceProvider.GetRequiredService(); + var options = serviceProvider.GetRequiredService>(); + if (!(httpContext?.Request.HasFormContentType ?? false)) + return Task.FromResult(false); + + if (httpContext?.Request.Form.ContainsKey(options.Value.TenantKey) ?? false) + { + var tenantId = httpContext.Request.Form[options.Value.TenantKey].ToString(); + if (!string.IsNullOrEmpty(tenantId)) + { + tenantSetter.SetTenant(new Tenant(tenantId)); + return Task.FromResult(true); + } + } + return Task.FromResult(false); + } +} diff --git a/src/Isolation/Masa.Contrib.Isolation.UoW.EF/Parser/MultiTenant/HeaderTenantParserProvider.cs b/src/Isolation/Masa.Contrib.Isolation.UoW.EF/Parser/MultiTenant/HeaderTenantParserProvider.cs new file mode 100644 index 000000000..db129a3f5 --- /dev/null +++ b/src/Isolation/Masa.Contrib.Isolation.UoW.EF/Parser/MultiTenant/HeaderTenantParserProvider.cs @@ -0,0 +1,24 @@ +namespace Masa.Contrib.Isolation.UoW.EF.Parser.MultiTenant; + +public class HeaderTenantParserProvider : ITenantParserProvider +{ + public string Name => "Header"; + + public Task ResolveAsync(IServiceProvider serviceProvider) + { + var httpContext = serviceProvider.GetService()?.HttpContext; + var tenantSetter = serviceProvider.GetRequiredService(); + var options = serviceProvider.GetRequiredService>(); + if (httpContext?.Request.Headers.ContainsKey(options.Value.TenantKey) ?? false) + { + var tenantId = httpContext.Request.Headers[options.Value.TenantKey].ToString(); + if (!string.IsNullOrEmpty(tenantId)) + { + tenantSetter.SetTenant(new Tenant(tenantId)); + return Task.FromResult(true); + } + } + + return Task.FromResult(false); + } +} diff --git a/src/Isolation/Masa.Contrib.Isolation.UoW.EF/Parser/MultiTenant/HttpContextItemTenantParserProvider.cs b/src/Isolation/Masa.Contrib.Isolation.UoW.EF/Parser/MultiTenant/HttpContextItemTenantParserProvider.cs new file mode 100644 index 000000000..102f56e04 --- /dev/null +++ b/src/Isolation/Masa.Contrib.Isolation.UoW.EF/Parser/MultiTenant/HttpContextItemTenantParserProvider.cs @@ -0,0 +1,24 @@ +namespace Masa.Contrib.Isolation.UoW.EF.Parser.MultiTenant; + +public class HttpContextItemTenantParserProvider : ITenantParserProvider +{ + public string Name => "Items"; + + public Task ResolveAsync(IServiceProvider serviceProvider) + { + var httpContext = serviceProvider.GetService()?.HttpContext; + var tenantSetter = serviceProvider.GetRequiredService(); + var options = serviceProvider.GetRequiredService>(); + if (httpContext?.Items.ContainsKey(options.Value.TenantKey) ?? false) + { + var tenantId = httpContext.Items[options.Value.TenantKey]?.ToString() ?? string.Empty; + if (!string.IsNullOrEmpty(tenantId)) + { + tenantSetter.SetTenant(new Tenant(tenantId)); + return Task.FromResult(true); + } + } + + return Task.FromResult(false); + } +} diff --git a/src/Isolation/Masa.Contrib.Isolation.UoW.EF/Parser/MultiTenant/QueryStringTenantParserProvider.cs b/src/Isolation/Masa.Contrib.Isolation.UoW.EF/Parser/MultiTenant/QueryStringTenantParserProvider.cs new file mode 100644 index 000000000..9fca23fb0 --- /dev/null +++ b/src/Isolation/Masa.Contrib.Isolation.UoW.EF/Parser/MultiTenant/QueryStringTenantParserProvider.cs @@ -0,0 +1,23 @@ +namespace Masa.Contrib.Isolation.UoW.EF.Parser.MultiTenant; + +public class QueryStringTenantParserProvider : ITenantParserProvider +{ + public string Name => "QueryString"; + + public Task ResolveAsync(IServiceProvider serviceProvider) + { + var httpContext = serviceProvider.GetService()?.HttpContext; + var tenantSetter = serviceProvider.GetRequiredService(); + var options = serviceProvider.GetRequiredService>(); + if (httpContext?.Request.Query.ContainsKey(options.Value.TenantKey) ?? false) + { + var tenantId = httpContext.Request.Query[options.Value.TenantKey].ToString(); + if (!string.IsNullOrEmpty(tenantId)) + { + tenantSetter.SetTenant(new Tenant(tenantId)); + return Task.FromResult(true); + } + } + return Task.FromResult(false); + } +} diff --git a/src/Isolation/Masa.Contrib.Isolation.UoW.EF/Parser/MultiTenant/RouteTenantParserProvider.cs b/src/Isolation/Masa.Contrib.Isolation.UoW.EF/Parser/MultiTenant/RouteTenantParserProvider.cs new file mode 100644 index 000000000..996817fde --- /dev/null +++ b/src/Isolation/Masa.Contrib.Isolation.UoW.EF/Parser/MultiTenant/RouteTenantParserProvider.cs @@ -0,0 +1,22 @@ +namespace Masa.Contrib.Isolation.UoW.EF.Parser.MultiTenant; + +public class RouteTenantParserProvider : ITenantParserProvider +{ + public string Name => "Route"; + + public Task ResolveAsync(IServiceProvider serviceProvider) + { + var httpContext = serviceProvider.GetService()?.HttpContext; + var tenantSetter = serviceProvider.GetRequiredService(); + var options = serviceProvider.GetRequiredService>(); + var tenantId = httpContext?.GetRouteValue(options.Value.TenantKey); + if (tenantId != null) + { + var tenantIdStr = tenantId.ToString(); + tenantSetter.SetTenant(new Tenant(tenantIdStr!)); + return Task.FromResult(true); + } + + return Task.FromResult(false); + } +} diff --git a/src/Isolation/Masa.Contrib.Isolation.UoW.EF/README.md b/src/Isolation/Masa.Contrib.Isolation.UoW.EF/README.md new file mode 100644 index 000000000..37af358e0 --- /dev/null +++ b/src/Isolation/Masa.Contrib.Isolation.UoW.EF/README.md @@ -0,0 +1,75 @@ +[中](README.zh-CN.md) | EN + +## Masa.Contrib.Isolation.UoW.EF + +Example: + +```C# +Install-Package Masa.Contrib.Isolation.UoW.EF +Install-Package Masa.Contrib.Isolation.Environment // Environmental isolation Quote on demand +Install-Package Masa.Contrib.Isolation.MultiTenant // Multi-tenant isolation On-demand reference +Install-Package Masa.Utils.Data.EntityFrameworkCore.SqlServer +``` + +1. 配置appsettings.json +``` appsettings.json +{ + "ConnectionStrings": { + "DefaultConnection": "server=localhost;uid=sa;pwd=P@ssw0rd;database=identity;", + "Isolations": [ + { + "TenantId": "*",// match all tenants + "Environment": "development", + "ConnectionString": "server=localhost,1672;uid=sa;pwd=P@ssw0rd;database=identity;", + "Score": 99 // When multiple environments are matched according to the conditions, the highest one is selected as the link address of the current DbContext according to the descending order of scores. The default Score is 100. + }, + { + "TenantId": "00000000-0000-0000-0000-000000000002", + "Environment": "development", + "ConnectionString": "server=localhost,1674;uid=sa;pwd=P@ssw0rd;database=identity;" + } + ] + } +} +``` + +* 1.1 When the current environment is equal to development: + * When the tenant is equal to 00000000-0000-0000-0000-000000000002, the database address: server=localhost,1674;uid=sa;pwd=P@ssw0rd;database=identity; + * When the tenant is not equal to 00000000-0000-0000-0000-000000000002, the database address: server=localhost,1672;uid=sa;pwd=P@ssw0rd;database=identity; + +* 1.2 When the environment is not equal to development: + * No tenant distinction, database address: server=localhost;uid=sa;pwd=P@ssw0rd;database=identity; + +2. 使用Isolation.UoW.EF +``` C# +builder.Services.AddEventBus(eventBusBuilder => +{ + eventBusBuilder.UseIsolationUoW( + dbOptions => dbOptions.UseSqlServer(), + isolationBuilder => isolationBuilder.UseEnvironment().UseMultiTenant());// Select usage environment or tenant isolation as needed +}); +``` + +3. DbContext needs to inherit IsolationDbContext + +``` C# +public class CustomDbContext : IsolationDbContext +{ + public CustomDbContext(MasaDbContextOptions options) : base(options) + { + } +} +``` + +4. The class corresponding to the isolated table needs to implement IMultiTenant or IMultiEnvironment + +Tenant isolation implements IMultiTenant, and environment isolation implements IMultiEnvironment + +##### Summarize +* How many kinds of parser are currently supported? + * Currently two kinds of parsers are supported, one is [Environment Parser](../Masa.Contrib.Isolation.Environment/README.zh-CN.md), the other is [Tenant Parser](../Masa.Contrib .Isolation.MultiTenant/README.zh-CN.md) +* How is the parser used? + * After publishing events through EventBus, EventBus will automatically call the parser middleware to trigger the environment and tenant parser to parse and assign values according to the isolation usage + * For scenarios where EventBus is not used, `app.UseIsolation();` needs to be added to Program.cs. After the request is initiated, it will first pass through the AspNetCore middleware provided by Isolation, and trigger the environment and tenant resolvers to parse and assign values. When the request arrives at the specified controller or Minimal method, the parsing is complete +* What does the parser do? + * Obtain the current environment and tenant information through the parser to provide data support for isolation, which needs to be called and executed before creating DbContext \ No newline at end of file diff --git a/src/Isolation/Masa.Contrib.Isolation.UoW.EF/README.zh-CN.md b/src/Isolation/Masa.Contrib.Isolation.UoW.EF/README.zh-CN.md new file mode 100644 index 000000000..8875d05cc --- /dev/null +++ b/src/Isolation/Masa.Contrib.Isolation.UoW.EF/README.zh-CN.md @@ -0,0 +1,75 @@ +中 | [EN](README.md) + +## Masa.Contrib.Isolation.UoW.EF + +用例: + +```C# +Install-Package Masa.Contrib.Isolation.UoW.EF +Install-Package Masa.Contrib.Isolation.Environment // 环境隔离 按需引用 +Install-Package Masa.Contrib.Isolation.MultiTenant // 多租户隔离 按需引用 +Install-Package Masa.Utils.Data.EntityFrameworkCore.SqlServer +``` + +1. 配置appsettings.json +``` appsettings.json +{ + "ConnectionStrings": { + "DefaultConnection": "server=localhost;uid=sa;pwd=P@ssw0rd;database=identity;", + "Isolations": [ + { + "TenantId": "*",//匹配所有租户 + "Environment": "development", + "ConnectionString": "server=localhost,1672;uid=sa;pwd=P@ssw0rd;database=identity;", + "Score": 99 //当根据条件匹配到多个环境时,根据分值降序选择其中最高的作为当前DbContext的链接地址,Score默认为100 + }, + { + "TenantId": "00000000-0000-0000-0000-000000000002", + "Environment": "development", + "ConnectionString": "server=localhost,1674;uid=sa;pwd=P@ssw0rd;database=identity;" + } + ] + } +} +``` + +* 1.1 当前环境等于development时: + * 当租户等于00000000-0000-0000-0000-000000000002时,数据库地址:server=localhost,1674;uid=sa;pwd=P@ssw0rd;database=identity; + * 当租户不等于00000000-0000-0000-0000-000000000002时,数据库地址:server=localhost,1672;uid=sa;pwd=P@ssw0rd;database=identity; + +* 1.2 当环境不等于development时: + * 不区分租户,数据库地址:server=localhost;uid=sa;pwd=P@ssw0rd;database=identity; + +2. 使用Isolation.UoW.EF +``` C# +builder.Services.AddEventBus(eventBusBuilder => +{ + eventBusBuilder.UseIsolationUoW( + dbOptions => dbOptions.UseSqlServer(), + isolationBuilder => isolationBuilder.UseEnvironment().UseMultiTenant());// 按需选择使用环境或者租户隔离 +}); +``` + +3. DbContext需要继承IsolationDbContext + +``` C# +public class CustomDbContext : IsolationDbContext +{ + public CustomDbContext(MasaDbContextOptions options) : base(options) + { + } +} +``` + +4. 隔离的表对应的类需要实现IMultiTenant或IMultiEnvironment + +租户隔离实现IMultiTenant、环境隔离实现IMultiEnvironment + +##### 总结 +* 解析器目前支持几种? + * 目前支持两种解析器,一个是[环境解析器](../Masa.Contrib.Isolation.Environment/README.zh-CN.md)、一个是[租户解析器](../Masa.Contrib.Isolation.MultiTenant/README.zh-CN.md) +* 解析器如何使用? + * 通过EventBus发布事件后,EventBus会自动调用解析器中间件,根据隔离性使用情况触发环境、租户解析器进行解析并赋值 + * 针对未使用EventBus的场景,需要在Program.cs中添加`app.UseIsolation();`,在请求发起后会先经过Isolation提供的AspNetCore中间件,并触发环境、租户解析器进行解析并赋值,当请求到达指定的控制器或者Minimal的方法时已经解析完成 +* 解析器的作用? + * 通过解析器获取当前的环境以及租户信息,为隔离提供数据支撑,需要在创建DbContext之前被调用执行 \ No newline at end of file diff --git a/src/Isolation/Masa.Contrib.Isolation.UoW.EF/_Imports.cs b/src/Isolation/Masa.Contrib.Isolation.UoW.EF/_Imports.cs new file mode 100644 index 000000000..956967146 --- /dev/null +++ b/src/Isolation/Masa.Contrib.Isolation.UoW.EF/_Imports.cs @@ -0,0 +1,24 @@ +global using Masa.BuildingBlocks.Data.UoW; +global using Masa.BuildingBlocks.Data.UoW.Options; +global using Masa.BuildingBlocks.Dispatcher.Events; +global using Masa.BuildingBlocks.Isolation; +global using Masa.BuildingBlocks.Isolation.Environment; +global using Masa.BuildingBlocks.Isolation.MultiTenant; +global using Masa.BuildingBlocks.Isolation.Options; +global using Masa.Contrib.Data.UoW.EF; +global using Masa.Contrib.Isolation.UoW.EF.Internal; +global using Masa.Contrib.Isolation.UoW.EF.Middleware; +global using Masa.Contrib.Isolation.UoW.EF.Parser.Environment; +global using Masa.Contrib.Isolation.UoW.EF.Parser.MultiTenant; +global using Masa.Utils.Data.EntityFrameworkCore; +global using Microsoft.AspNetCore.Builder; +global using Microsoft.AspNetCore.Http; +global using Microsoft.AspNetCore.Routing; +global using Microsoft.Extensions.Configuration; +global using Microsoft.Extensions.DependencyInjection; +global using Microsoft.Extensions.DependencyInjection.Extensions; +global using Microsoft.Extensions.Logging; +global using Microsoft.Extensions.Options; +global using System.Linq.Expressions; +global using System.Reflection; +global using System.Text; diff --git a/test/Masa.Contrib.Data.UoW.EF.Tests/Masa.Contrib.Data.UoW.EF.Tests.csproj b/test/Masa.Contrib.Data.UoW.EF.Tests/Masa.Contrib.Data.UoW.EF.Tests.csproj index 1a9474e95..77fdd7472 100644 --- a/test/Masa.Contrib.Data.UoW.EF.Tests/Masa.Contrib.Data.UoW.EF.Tests.csproj +++ b/test/Masa.Contrib.Data.UoW.EF.Tests/Masa.Contrib.Data.UoW.EF.Tests.csproj @@ -13,7 +13,6 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/test/Masa.Contrib.Data.UoW.EF.Tests/TestUnitOfWork.cs b/test/Masa.Contrib.Data.UoW.EF.Tests/TestUnitOfWork.cs index cd8a3b4eb..1ca643d71 100644 --- a/test/Masa.Contrib.Data.UoW.EF.Tests/TestUnitOfWork.cs +++ b/test/Masa.Contrib.Data.UoW.EF.Tests/TestUnitOfWork.cs @@ -160,7 +160,7 @@ public void TestDataConnectionString() } [TestMethod] - public async Task TestUnitOfWorkManagerAsync() + public void TestUnitOfWorkManager() { _options.Object.UseUoW(options => options.UseSqlite(Connection)); var serviceProvider = _options.Object.Services.BuildServiceProvider(); @@ -194,11 +194,12 @@ public async Task TestUnitOfWorkAccessorAsync() Assert.IsTrue(unitOfWorkAccessor is { CurrentDbContextOptions: null }); var unitOfWork = serviceProvider.GetRequiredService(); Assert.IsNotNull(unitOfWork); + Assert.IsTrue(!unitOfWork.TransactionHasBegun); unitOfWorkAccessor = serviceProvider.GetService(); Assert.IsTrue(unitOfWorkAccessor!.CurrentDbContextOptions != null && unitOfWorkAccessor.CurrentDbContextOptions.ConnectionString == configurationRoot["ConnectionStrings:DefaultConnection"].ToString()); var unitOfWorkManager = serviceProvider.GetRequiredService(); - var unitOfWorkNew = unitOfWorkManager.CreateDbContext(); + var unitOfWorkNew = unitOfWorkManager.CreateDbContext(false); var unitOfWorkAccessorNew = unitOfWorkNew.ServiceProvider.GetService(); Assert.IsTrue(unitOfWorkAccessorNew!.CurrentDbContextOptions != null && unitOfWorkAccessorNew.CurrentDbContextOptions.ConnectionString == configurationRoot["ConnectionStrings:DefaultConnection"].ToString()); diff --git a/test/Masa.Contrib.Data.UoW.EF.Tests/_Imports.cs b/test/Masa.Contrib.Data.UoW.EF.Tests/_Imports.cs index 76de8a730..3661d318f 100644 --- a/test/Masa.Contrib.Data.UoW.EF.Tests/_Imports.cs +++ b/test/Masa.Contrib.Data.UoW.EF.Tests/_Imports.cs @@ -5,9 +5,9 @@ global using Microsoft.Data.Sqlite; global using Microsoft.EntityFrameworkCore; global using Microsoft.EntityFrameworkCore.Metadata.Builders; +global using Microsoft.Extensions.Configuration; global using Microsoft.Extensions.DependencyInjection; global using Microsoft.VisualStudio.TestTools.UnitTesting; global using Moq; global using System; global using System.Threading.Tasks; -global using Microsoft.Extensions.Configuration; diff --git a/test/Masa.Contrib.Dispatcher.Events.Tests/AssemblyResolutionTests.cs b/test/Masa.Contrib.Dispatcher.Events.Tests/AssemblyResolutionTests.cs index cb70a8628..ee0096135 100644 --- a/test/Masa.Contrib.Dispatcher.Events.Tests/AssemblyResolutionTests.cs +++ b/test/Masa.Contrib.Dispatcher.Events.Tests/AssemblyResolutionTests.cs @@ -23,7 +23,7 @@ public void TestAddNullAssembly() services.AddLogging(loggingBuilder => loggingBuilder.AddConsole()); Assert.ThrowsException(() => { - Assembly[] assemblies = null; + Assembly[] assemblies = null!; services.AddEventBus(assemblies!); }); } @@ -46,7 +46,7 @@ public void TestEventBusByAddNullAssembly() services.AddLogging(loggingBuilder => loggingBuilder.AddConsole()); Assert.ThrowsException(() => { - services.AddTestEventBus(null, ServiceLifetime.Scoped); + services.AddTestEventBus(null!, ServiceLifetime.Scoped); }); } diff --git a/test/Masa.Contrib.Dispatcher.IntegrationEvents.Dapr.Tests/IntegrationEventBusTest.cs b/test/Masa.Contrib.Dispatcher.IntegrationEvents.Dapr.Tests/IntegrationEventBusTest.cs index 0e19ec7d0..9a43b4761 100644 --- a/test/Masa.Contrib.Dispatcher.IntegrationEvents.Dapr.Tests/IntegrationEventBusTest.cs +++ b/test/Masa.Contrib.Dispatcher.IntegrationEvents.Dapr.Tests/IntegrationEventBusTest.cs @@ -117,7 +117,7 @@ public void TestUseLogger() [TestMethod] public void TestAddDaprEventBusAndNullServicesAsync() { - IServiceCollection services = null; + IServiceCollection services = null!; Mock distributedDispatcherOptions = new(); distributedDispatcherOptions.Setup(option => option.Services).Returns(services).Verifiable(); distributedDispatcherOptions.Setup(option => option.Assemblies).Returns(AppDomain.CurrentDomain.GetAssemblies()).Verifiable(); diff --git a/test/Masa.Contrib.Dispatcher.IntegrationEvents.EventLogs.EF.Tests/IntegrationEventLogServiceTest.cs b/test/Masa.Contrib.Dispatcher.IntegrationEvents.EventLogs.EF.Tests/IntegrationEventLogServiceTest.cs index a0d18f677..94694a347 100644 --- a/test/Masa.Contrib.Dispatcher.IntegrationEvents.EventLogs.EF.Tests/IntegrationEventLogServiceTest.cs +++ b/test/Masa.Contrib.Dispatcher.IntegrationEvents.EventLogs.EF.Tests/IntegrationEventLogServiceTest.cs @@ -27,7 +27,7 @@ public async Task TestNullDbTransactionAsync() [TestMethod] public void TestNullServices() { - var dispatcherOptions = CreateDispatcherOptions(null); + var dispatcherOptions = CreateDispatcherOptions(null!); Assert.ThrowsException(() => { @@ -84,10 +84,10 @@ await response.CustomDbContext.Set().AddAsync(new Integrati #endregion var logService = response.ServiceProvider.GetRequiredService(); - var list = await logService.RetrieveEventLogsFailedToPublishAsync(); - Assert.IsTrue(list.Count() == 1); + var list = (await logService.RetrieveEventLogsFailedToPublishAsync()).ToList(); + Assert.IsTrue(list.Count == 1); - var eventLog = list.Select(log => log.Event).FirstOrDefault(); + var eventLog = list.Select(log => log.Event).FirstOrDefault()!; Assert.IsTrue(eventLog.Equals(@event)); } diff --git a/test/Masa.Contrib.Isolation.UoW.EF.Tests/CustomDbContext.cs b/test/Masa.Contrib.Isolation.UoW.EF.Tests/CustomDbContext.cs new file mode 100644 index 000000000..7d7594738 --- /dev/null +++ b/test/Masa.Contrib.Isolation.UoW.EF.Tests/CustomDbContext.cs @@ -0,0 +1,39 @@ +namespace Masa.Contrib.Isolation.UoW.EF.Tests; + +public class CustomDbContext : MasaDbContext +{ + public CustomDbContext(MasaDbContextOptions options) : base(options) { } + + public DbSet User { get; set; } + + protected override void OnModelCreatingExecuting(ModelBuilder builder) + { + builder.Entity(ConfigureUserEntry); + } + + void ConfigureUserEntry(EntityTypeBuilder builder) + { + builder.ToTable("Users"); + + builder.HasKey(e => e.Id); + + builder.Property(e => e.Id) + .IsRequired(); + + builder.Property(e => e.Name) + .HasMaxLength(6) + .IsRequired(); + } +} + +public class Users +{ + public Guid Id { get; private set; } + + public string Name { get; set; } = default!; + + public Users() + { + this.Id = Guid.NewGuid(); + } +} diff --git a/test/Masa.Contrib.Isolation.UoW.EF.Tests/Masa.Contrib.Isolation.UoW.EF.Tests.csproj b/test/Masa.Contrib.Isolation.UoW.EF.Tests/Masa.Contrib.Isolation.UoW.EF.Tests.csproj new file mode 100644 index 000000000..4abf62d6c --- /dev/null +++ b/test/Masa.Contrib.Isolation.UoW.EF.Tests/Masa.Contrib.Isolation.UoW.EF.Tests.csproj @@ -0,0 +1,35 @@ + + + + net6.0 + enable + false + enable + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + Always + + + + diff --git a/test/Masa.Contrib.Isolation.UoW.EF.Tests/RequestCookieCollection.cs b/test/Masa.Contrib.Isolation.UoW.EF.Tests/RequestCookieCollection.cs new file mode 100644 index 000000000..16401caf8 --- /dev/null +++ b/test/Masa.Contrib.Isolation.UoW.EF.Tests/RequestCookieCollection.cs @@ -0,0 +1,6 @@ +namespace Masa.Contrib.Isolation.UoW.EF.Tests; + +public class RequestCookieCollection : Dictionary, IRequestCookieCollection +{ + public new ICollection Keys { get; } +} diff --git a/test/Masa.Contrib.Isolation.UoW.EF.Tests/TestBase.cs b/test/Masa.Contrib.Isolation.UoW.EF.Tests/TestBase.cs new file mode 100644 index 000000000..02cc6e61d --- /dev/null +++ b/test/Masa.Contrib.Isolation.UoW.EF.Tests/TestBase.cs @@ -0,0 +1,18 @@ +namespace Masa.Contrib.Isolation.UoW.EF.Tests; + +public class TestBase : IDisposable +{ + protected readonly string _connectionString = "DataSource=:memory:"; + protected readonly SqliteConnection Connection; + + protected TestBase() + { + Connection = new SqliteConnection(_connectionString); + Connection.Open(); + } + + public void Dispose() + { + Connection.Close(); + } +} diff --git a/test/Masa.Contrib.Isolation.UoW.EF.Tests/TestIsolation.cs b/test/Masa.Contrib.Isolation.UoW.EF.Tests/TestIsolation.cs new file mode 100644 index 000000000..62f9b5297 --- /dev/null +++ b/test/Masa.Contrib.Isolation.UoW.EF.Tests/TestIsolation.cs @@ -0,0 +1,824 @@ +using Masa.Contrib.Isolation.MultiTenant; +using Masa.Contrib.Isolation.UoW.EF.Parser.MultiTenant; + +namespace Masa.Contrib.Isolation.UoW.EF.Tests; + +[TestClass] +public class TestIsolation : TestBase +{ + private IServiceCollection _services; + + [TestInitialize] + public void Initialize() + { + _services = new ServiceCollection(); + } + + [TestMethod] + public void TestUseIsolationUoW() + { + Mock eventBuilder = new(); + eventBuilder.Setup(builder => builder.Services).Returns(_services).Verifiable(); + Assert.ThrowsException(() => + { + eventBuilder.Object.UseIsolationUoW(dbOptionBuilder => dbOptionBuilder.UseSqlite(_connectionString), _ => + { + }); + }, "Tenant isolation and environment isolation use at least one"); + } + + [TestMethod] + public void TestUseIsolationUoW2() + { + Mock eventBuilder = new(); + eventBuilder.Setup(builder => builder.Services).Returns(_services).Verifiable(); + Assert.ThrowsException(() => + { + eventBuilder.Object.UseIsolationUoW(dbOptionBuilder => dbOptionBuilder.UseSqlite(_connectionString), null!); + }); + } + + [TestMethod] + public void TestUseIsolationUoW3() + { + Mock dispatcherOption = new(); + dispatcherOption.Setup(builder => builder.Services).Returns(_services).Verifiable(); + Assert.ThrowsException(() => + { + dispatcherOption.Object.UseIsolationUoW(dbOptionBuilder => dbOptionBuilder.UseSqlite(_connectionString), _ => + { + }); + }, "Tenant isolation and environment isolation use at least one"); + } + + [TestMethod] + public void TestUseIsolationUoW4() + { + Mock dispatcherOption = new(); + dispatcherOption.Setup(builder => builder.Services).Returns(_services).Verifiable(); + Assert.ThrowsException(() => + { + dispatcherOption.Object.UseIsolationUoW(dbOptionBuilder => dbOptionBuilder.UseSqlite(), null!); + }); + } + + [TestMethod] + public void TestUseIsolationUoWByUseEnvironment() + { + Mock dispatcherOption = new(); + dispatcherOption.Setup(builder => builder.Services).Returns(_services).Verifiable(); + dispatcherOption.Object.UseIsolationUoW(dbOptionBuilder => dbOptionBuilder.UseSqlite(_connectionString), + isolationBuilder => isolationBuilder.UseEnvironment()); + + var serviceProvider = dispatcherOption.Object.Services.BuildServiceProvider(); + Assert.IsNotNull(serviceProvider.GetService()); + Assert.IsNotNull(serviceProvider.GetService()); + } + + [TestMethod] + public void TestUseIsolationUoWByUseMultiEnvironment() + { + Mock dispatcherOption = new(); + dispatcherOption.Setup(builder => builder.Services).Returns(_services).Verifiable(); + dispatcherOption.Object.UseIsolationUoW(dbOptionBuilder => dbOptionBuilder.UseSqlite(_connectionString), + isolationBuilder => isolationBuilder.UseEnvironment().UseEnvironment()); + + var serviceProvider = dispatcherOption.Object.Services.BuildServiceProvider(); + Assert.IsTrue(serviceProvider.GetServices().Count() == 1); + Assert.IsTrue(serviceProvider.GetServices().Count() == 1); + } + + [TestMethod] + public void TestUseIsolationUoWByUseTenant() + { + Mock dispatcherOption = new(); + dispatcherOption.Setup(builder => builder.Services).Returns(_services).Verifiable(); + dispatcherOption.Object.UseIsolationUoW(dbOptionBuilder => dbOptionBuilder.UseSqlite(_connectionString), + isolationBuilder => isolationBuilder.UseMultiTenant()); + + var serviceProvider = dispatcherOption.Object.Services.BuildServiceProvider(); + Assert.IsNotNull(serviceProvider.GetService()); + Assert.IsNotNull(serviceProvider.GetService()); + } + + [TestMethod] + public void TestUseIsolationUoWByUseMultiTenant() + { + Mock dispatcherOption = new(); + dispatcherOption.Setup(builder => builder.Services).Returns(_services).Verifiable(); + dispatcherOption.Object.UseIsolationUoW(dbOptionBuilder => dbOptionBuilder.UseSqlite(_connectionString), + isolationBuilder => isolationBuilder.UseMultiTenant().UseMultiTenant()); + + var serviceProvider = dispatcherOption.Object.Services.BuildServiceProvider(); + Assert.IsTrue(serviceProvider.GetServices().Count() == 1); + Assert.IsTrue(serviceProvider.GetServices().Count() == 1); + } + + [TestMethod] + public void TestUseIsolation() + { + var configurationRoot = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json", true, true) + .Build(); + _services.AddSingleton(configurationRoot); + Mock dispatcherOption = new(); + dispatcherOption.Setup(builder => builder.Services).Returns(_services).Verifiable(); + dispatcherOption.Object.UseIsolationUoW(dbOptionBuilder => dbOptionBuilder.UseSqlite(), + isolationBuilder => isolationBuilder.UseMultiTenant().UseEnvironment()); + var serviceProvider = _services.BuildServiceProvider(); + var customDbContext = serviceProvider.GetRequiredService(); + var unitOfWorkAccessor = serviceProvider.GetRequiredService(); + var currentDbContextOptions = unitOfWorkAccessor.CurrentDbContextOptions; + Assert.IsNotNull(currentDbContextOptions); + Assert.IsTrue(currentDbContextOptions.ConnectionString == "data source=test1"); + + var unitOfWorkManager = serviceProvider.GetRequiredService(); + var unifOfWorkNew = unitOfWorkManager.CreateDbContext(true); + var unitOfWorkAccessorNew = unifOfWorkNew.ServiceProvider.GetRequiredService(); + + Assert.IsNull(unitOfWorkAccessorNew.CurrentDbContextOptions); + + Assert.IsTrue(unifOfWorkNew.ServiceProvider.GetRequiredService().CurrentTenant == null); + + Assert.IsTrue(string.IsNullOrEmpty(unifOfWorkNew.ServiceProvider.GetRequiredService().CurrentEnvironment)); + + unifOfWorkNew.ServiceProvider.GetRequiredService().SetTenant(new Tenant("00000000-0000-0000-0000-000000000002")); + Assert.IsTrue(unifOfWorkNew.ServiceProvider.GetRequiredService().CurrentTenant!.Id == + "00000000-0000-0000-0000-000000000002"); + unifOfWorkNew.ServiceProvider.GetRequiredService().SetEnvironment("dev"); + + Assert.IsTrue(unifOfWorkNew.ServiceProvider.GetRequiredService().CurrentEnvironment == "dev"); + + var dbContext = unifOfWorkNew.ServiceProvider.GetRequiredService(); + + Assert.IsTrue(GetDataBaseConnectionString(dbContext) == "data source=test1" && + unitOfWorkAccessorNew.CurrentDbContextOptions!.ConnectionString == "data source=test1"); + + var unifOfWorkNew2 = unitOfWorkManager.CreateDbContext(true); + var unitOfWorkAccessorNew2 = unifOfWorkNew2.ServiceProvider.GetRequiredService(); + unifOfWorkNew2.ServiceProvider.GetRequiredService().SetEnvironment("development"); + var dbContext2 = unifOfWorkNew2.ServiceProvider.GetRequiredService(); + Assert.IsTrue(GetDataBaseConnectionString(dbContext2) == "data source=test1" && + unitOfWorkAccessorNew2.CurrentDbContextOptions!.ConnectionString == "data source=test1"); + + var unifOfWorkNew3 = unitOfWorkManager.CreateDbContext(true); + var unitOfWorkAccessorNew3 = unifOfWorkNew3.ServiceProvider.GetRequiredService(); + unifOfWorkNew3.ServiceProvider.GetRequiredService().SetTenant(new Tenant("00000000-0000-0000-0000-000000000002")); + unifOfWorkNew3.ServiceProvider.GetRequiredService().SetEnvironment("development"); + var dbContext3 = unifOfWorkNew3.ServiceProvider.GetRequiredService(); + Assert.IsTrue(GetDataBaseConnectionString(dbContext3) == "data source=test2" && + unitOfWorkAccessorNew3.CurrentDbContextOptions!.ConnectionString == "data source=test2"); + + var unifOfWorkNew4 = unitOfWorkManager.CreateDbContext(true); + var unitOfWorkAccessorNew4 = unifOfWorkNew4.ServiceProvider.GetRequiredService(); + unifOfWorkNew4.ServiceProvider.GetRequiredService().SetTenant(new Tenant("00000000-0000-0000-0000-000000000002")); + unifOfWorkNew4.ServiceProvider.GetRequiredService().SetEnvironment("production"); + var dbContext4 = unifOfWorkNew4.ServiceProvider.GetRequiredService(); + Assert.IsTrue(GetDataBaseConnectionString(dbContext4) == "data source=test3" && + unitOfWorkAccessorNew4.CurrentDbContextOptions!.ConnectionString == "data source=test3"); + } + + [TestMethod] + public void TestUseEnvironment() + { + _services.Configure(option => + { + option.DefaultConnection = "data source=test4"; + option.Isolations = new List() + { + new() + { + ConnectionString = "data source=test5", + Environment = "dev" + }, + new() + { + ConnectionString = "data source=test6", + Environment = "pro" + } + }; + }); + Mock dispatcherOption = new(); + dispatcherOption.Setup(builder => builder.Services).Returns(_services).Verifiable(); + dispatcherOption.Object.UseIsolationUoW(dbOptionBuilder => dbOptionBuilder.UseSqlite(), + isolationBuilder => isolationBuilder.UseEnvironment()); + var serviceProvider = _services.BuildServiceProvider(); + var customDbContext = serviceProvider.GetRequiredService(); + var unitOfWorkAccessor = serviceProvider.GetRequiredService(); + var currentDbContextOptions = unitOfWorkAccessor.CurrentDbContextOptions; + Assert.IsNotNull(currentDbContextOptions); + Assert.IsTrue(currentDbContextOptions.ConnectionString == "data source=test4"); + + var unitOfWorkManager = serviceProvider.GetRequiredService(); + + var unifOfWorkNew2 = unitOfWorkManager.CreateDbContext(true); + var unitOfWorkAccessorNew2 = unifOfWorkNew2.ServiceProvider.GetRequiredService(); + unifOfWorkNew2.ServiceProvider.GetRequiredService().SetEnvironment("dev"); + var dbContext2 = unifOfWorkNew2.ServiceProvider.GetRequiredService(); + Assert.IsTrue(GetDataBaseConnectionString(dbContext2) == "data source=test5" && + unitOfWorkAccessorNew2.CurrentDbContextOptions!.ConnectionString == "data source=test5"); + + var unifOfWorkNew3 = unitOfWorkManager.CreateDbContext(true); + var unitOfWorkAccessorNew3 = unifOfWorkNew3.ServiceProvider.GetRequiredService(); + unifOfWorkNew3.ServiceProvider.GetRequiredService().SetEnvironment("pro"); + var dbContext3 = unifOfWorkNew3.ServiceProvider.GetRequiredService(); + Assert.IsTrue(GetDataBaseConnectionString(dbContext3) == "data source=test6" && + unitOfWorkAccessorNew3.CurrentDbContextOptions!.ConnectionString == "data source=test6"); + + var unifOfWorkNew4 = unitOfWorkManager.CreateDbContext(true); + var unitOfWorkAccessorNew4 = unifOfWorkNew4.ServiceProvider.GetRequiredService(); + unifOfWorkNew4.ServiceProvider.GetRequiredService().SetEnvironment("staging"); + var dbContext4 = unifOfWorkNew4.ServiceProvider.GetRequiredService(); + Assert.IsTrue(GetDataBaseConnectionString(dbContext4) == "data source=test4" && + unitOfWorkAccessorNew4.CurrentDbContextOptions!.ConnectionString == "data source=test4"); + } + + [TestMethod] + public void TestUseMultiTenant() + { + _services.Configure(option => + { + option.DefaultConnection = "data source=test7"; + option.Isolations = new List() + { + new() + { + ConnectionString = "data source=test8", + TenantId = "1" + }, + new() + { + ConnectionString = "data source=test9", + TenantId = "2" + } + }; + }); + Mock dispatcherOption = new(); + dispatcherOption.Setup(builder => builder.Services).Returns(_services).Verifiable(); + dispatcherOption.Object.UseIsolationUoW(dbOptionBuilder => dbOptionBuilder.UseSqlite(), + isolationBuilder => isolationBuilder.UseMultiTenant()); + var serviceProvider = _services.BuildServiceProvider(); + var customDbContext = serviceProvider.GetRequiredService(); + var unitOfWorkAccessor = serviceProvider.GetRequiredService(); + var currentDbContextOptions = unitOfWorkAccessor.CurrentDbContextOptions; + Assert.IsNotNull(currentDbContextOptions); + Assert.IsTrue(currentDbContextOptions.ConnectionString == "data source=test7"); + + var unitOfWorkManager = serviceProvider.GetRequiredService(); + + var unifOfWorkNew2 = unitOfWorkManager.CreateDbContext(true); + var unitOfWorkAccessorNew2 = unifOfWorkNew2.ServiceProvider.GetRequiredService(); + unifOfWorkNew2.ServiceProvider.GetRequiredService().SetTenant(new Tenant("1")); + var dbContext2 = unifOfWorkNew2.ServiceProvider.GetRequiredService(); + Assert.IsTrue(GetDataBaseConnectionString(dbContext2) == "data source=test8" && + unitOfWorkAccessorNew2.CurrentDbContextOptions!.ConnectionString == "data source=test8"); + + var unifOfWorkNew3 = unitOfWorkManager.CreateDbContext(true); + var unitOfWorkAccessorNew3 = unifOfWorkNew3.ServiceProvider.GetRequiredService(); + unifOfWorkNew3.ServiceProvider.GetRequiredService().SetTenant(new Tenant("2")); + var dbContext3 = unifOfWorkNew3.ServiceProvider.GetRequiredService(); + Assert.IsTrue(GetDataBaseConnectionString(dbContext3) == "data source=test9" && + unitOfWorkAccessorNew3.CurrentDbContextOptions!.ConnectionString == "data source=test9"); + + var unifOfWorkNew4 = unitOfWorkManager.CreateDbContext(true); + var unitOfWorkAccessorNew4 = unifOfWorkNew4.ServiceProvider.GetRequiredService(); + unifOfWorkNew4.ServiceProvider.GetRequiredService().SetTenant(null!); + var dbContext4 = unifOfWorkNew4.ServiceProvider.GetRequiredService(); + Assert.IsTrue(GetDataBaseConnectionString(dbContext4) == "data source=test7" && + unitOfWorkAccessorNew4.CurrentDbContextOptions!.ConnectionString == "data source=test7"); + } + + [TestMethod] + public void TestIsolationBuilder() + { + var services = new ServiceCollection(); + var isolationBuilder = new IsolationBuilder(services); + Assert.IsTrue(isolationBuilder.EnvironmentKey == "ASPNETCORE_ENVIRONMENT"); + Assert.IsTrue(isolationBuilder.TenantKey == "__tenant"); + Assert.IsTrue(isolationBuilder.TenantParsers.Count == 6); + Assert.IsTrue(isolationBuilder.EnvironmentParsers.Count == 1); + + Assert.IsTrue(isolationBuilder.SetTenantKey("tenantId").TenantKey == "tenantId"); + Assert.IsTrue(isolationBuilder.SetEnvironmentKey("dev").EnvironmentKey == "dev"); + Assert.IsTrue(isolationBuilder.SetEnvironmentParsers(new List()).EnvironmentParsers.Count == 0); + Assert.IsTrue(isolationBuilder.SetTenantParsers(new List()).EnvironmentParsers.Count == 0); + } + + [TestMethod] + public async Task TestCookieTenantParserAsync() + { + var services = new ServiceCollection(); + services.AddHttpContextAccessor(); + string tenantKey = "tenant"; + Mock tenantSetter = new(); + tenantSetter.Setup(setter => setter.SetTenant(It.IsAny())).Verifiable(); + services.AddScoped(_ => tenantSetter.Object); + services.Configure(option => + { + option.TenantKey = tenantKey; + }); + var serviceProvider = services.BuildServiceProvider(); + var httpContextAccessor = serviceProvider.GetRequiredService(); + httpContextAccessor.HttpContext = new DefaultHttpContext() + { + Request = + { + Cookies = new RequestCookieCollection + { + { + tenantKey, "1" + } + } + } + }; + var provider = new CookieTenantParserProvider(); + Assert.IsTrue(provider.Name == "Cookie"); + var handler = await provider.ResolveAsync(services.BuildServiceProvider()); + Assert.IsTrue(handler); + tenantSetter.Verify(setter => setter.SetTenant(It.IsAny()), Times.Once); + } + + [TestMethod] + public async Task TestCookieTenantParser2Async() + { + var services = new ServiceCollection(); + services.AddHttpContextAccessor(); + string tenantKey = "tenant"; + Mock tenantSetter = new(); + tenantSetter.Setup(setter => setter.SetTenant(It.IsAny())).Verifiable(); + services.AddScoped(_ => tenantSetter.Object); + services.Configure(option => + { + option.TenantKey = tenantKey; + }); + var serviceProvider = services.BuildServiceProvider(); + var httpContextAccessor = serviceProvider.GetRequiredService(); + httpContextAccessor.HttpContext = new DefaultHttpContext() + { + Request = + { + Cookies = new RequestCookieCollection() + } + }; + var provider = new CookieTenantParserProvider(); + var handler = await provider.ResolveAsync(services.BuildServiceProvider()); + Assert.IsFalse(handler); + tenantSetter.Verify(setter => setter.SetTenant(It.IsAny()), Times.Never); + } + + [TestMethod] + public async Task TestCookieTenantParser3Async() + { + var services = new ServiceCollection(); + string tenantKey = "tenant"; + Mock tenantSetter = new(); + tenantSetter.Setup(setter => setter.SetTenant(It.IsAny())).Verifiable(); + services.AddScoped(_ => tenantSetter.Object); + services.Configure(option => + { + option.TenantKey = tenantKey; + }); + var provider = new CookieTenantParserProvider(); + var handler = await provider.ResolveAsync(services.BuildServiceProvider()); + Assert.IsFalse(handler); + tenantSetter.Verify(setter => setter.SetTenant(It.IsAny()), Times.Never); + } + + [TestMethod] + public async Task TestFormTenantParserAsync() + { + var services = new ServiceCollection(); + services.AddHttpContextAccessor(); + string tenantKey = "tenant"; + Mock tenantSetter = new(); + tenantSetter.Setup(setter => setter.SetTenant(It.IsAny())).Verifiable(); + services.AddScoped(_ => tenantSetter.Object); + services.Configure(option => + { + option.TenantKey = tenantKey; + }); + var serviceProvider = services.BuildServiceProvider(); + var httpContextAccessor = serviceProvider.GetRequiredService(); + httpContextAccessor.HttpContext = new DefaultHttpContext() + { + Request = + { + Form = new FormCollection(new Dictionary() + { + { tenantKey, "1" } + } + ) + } + }; + var provider = new FormTenantParserProvider(); + Assert.IsTrue(provider.Name == "Form"); + var handler = await provider.ResolveAsync(services.BuildServiceProvider()); + Assert.IsTrue(handler); + tenantSetter.Verify(setter => setter.SetTenant(It.IsAny()), Times.Once); + } + + [TestMethod] + public async Task TestFormTenantParser2Async() + { + var services = new ServiceCollection(); + services.AddHttpContextAccessor(); + string tenantKey = "tenant"; + Mock tenantSetter = new(); + tenantSetter.Setup(setter => setter.SetTenant(It.IsAny())).Verifiable(); + services.AddScoped(_ => tenantSetter.Object); + services.Configure(option => + { + option.TenantKey = tenantKey; + }); + var serviceProvider = services.BuildServiceProvider(); + var httpContextAccessor = serviceProvider.GetRequiredService(); + httpContextAccessor.HttpContext = new DefaultHttpContext() + { + Request = + { + Form = new FormCollection(new Dictionary()) + } + }; + var provider = new FormTenantParserProvider(); + var handler = await provider.ResolveAsync(services.BuildServiceProvider()); + Assert.IsFalse(handler); + tenantSetter.Verify(setter => setter.SetTenant(It.IsAny()), Times.Never); + } + + [TestMethod] + public async Task TestFormTenantParser3Async() + { + var services = new ServiceCollection(); + services.AddHttpContextAccessor(); + string tenantKey = "tenant"; + Mock tenantSetter = new(); + tenantSetter.Setup(setter => setter.SetTenant(It.IsAny())).Verifiable(); + services.AddScoped(_ => tenantSetter.Object); + services.Configure(option => + { + option.TenantKey = tenantKey; + }); + var serviceProvider = services.BuildServiceProvider(); + var httpContextAccessor = serviceProvider.GetRequiredService(); + httpContextAccessor.HttpContext = new DefaultHttpContext() + { + Request = + { + QueryString = QueryString.Create(tenantKey, "1") + } + }; + var provider = new FormTenantParserProvider(); + Assert.IsTrue(provider.Name == "Form"); + var handler = await provider.ResolveAsync(services.BuildServiceProvider()); + Assert.IsFalse(handler); + tenantSetter.Verify(setter => setter.SetTenant(It.IsAny()), Times.Never); + } + + [TestMethod] + public async Task TestHeaderTenantParserAsync() + { + var services = new ServiceCollection(); + services.AddHttpContextAccessor(); + string tenantKey = "tenant"; + Mock tenantSetter = new(); + tenantSetter.Setup(setter => setter.SetTenant(It.IsAny())).Verifiable(); + services.AddScoped(_ => tenantSetter.Object); + services.Configure(option => + { + option.TenantKey = tenantKey; + }); + var serviceProvider = services.BuildServiceProvider(); + var httpContextAccessor = serviceProvider.GetRequiredService(); + httpContextAccessor.HttpContext = new DefaultHttpContext() + { + Request = + { + Headers = + { + { tenantKey, "1" } + } + } + }; + var provider = new HeaderTenantParserProvider(); + Assert.IsTrue(provider.Name == "Header"); + var handler = await provider.ResolveAsync(services.BuildServiceProvider()); + Assert.IsTrue(handler); + tenantSetter.Verify(setter => setter.SetTenant(It.IsAny()), Times.Once); + } + + [TestMethod] + public async Task TestHeaderTenantParser2Async() + { + var services = new ServiceCollection(); + services.AddHttpContextAccessor(); + string tenantKey = "tenant"; + Mock tenantSetter = new(); + tenantSetter.Setup(setter => setter.SetTenant(It.IsAny())).Verifiable(); + services.AddScoped(_ => tenantSetter.Object); + services.Configure(option => + { + option.TenantKey = tenantKey; + }); + var serviceProvider = services.BuildServiceProvider(); + var httpContextAccessor = serviceProvider.GetRequiredService(); + httpContextAccessor.HttpContext = new DefaultHttpContext() + { + Request = + { + Headers = { } + } + }; + var provider = new HeaderTenantParserProvider(); + var handler = await provider.ResolveAsync(services.BuildServiceProvider()); + Assert.IsFalse(handler); + tenantSetter.Verify(setter => setter.SetTenant(It.IsAny()), Times.Never); + } + + [TestMethod] + public async Task TestHttpContextItemTenantParserAsync() + { + var services = new ServiceCollection(); + services.AddHttpContextAccessor(); + string tenantKey = "tenant"; + Mock tenantSetter = new(); + tenantSetter.Setup(setter => setter.SetTenant(It.IsAny())).Verifiable(); + services.AddScoped(_ => tenantSetter.Object); + services.Configure(option => + { + option.TenantKey = tenantKey; + }); + var serviceProvider = services.BuildServiceProvider(); + var httpContextAccessor = serviceProvider.GetRequiredService(); + httpContextAccessor.HttpContext = new DefaultHttpContext() + { + Items = new Dictionary() + { + { tenantKey, "1" } + } + }; + var provider = new HttpContextItemTenantParserProvider(); + Assert.IsTrue(provider.Name == "Items"); + var handler = await provider.ResolveAsync(services.BuildServiceProvider()); + Assert.IsTrue(handler); + tenantSetter.Verify(setter => setter.SetTenant(It.IsAny()), Times.Once); + } + + [TestMethod] + public async Task TestHttpContextItemTenantParser2Async() + { + var services = new ServiceCollection(); + services.AddHttpContextAccessor(); + string tenantKey = "tenant"; + Mock tenantSetter = new(); + tenantSetter.Setup(setter => setter.SetTenant(It.IsAny())).Verifiable(); + services.AddScoped(_ => tenantSetter.Object); + services.Configure(option => + { + option.TenantKey = tenantKey; + }); + var serviceProvider = services.BuildServiceProvider(); + var httpContextAccessor = serviceProvider.GetRequiredService(); + httpContextAccessor.HttpContext = new DefaultHttpContext() + { + Items = new Dictionary() + }; + var provider = new HttpContextItemTenantParserProvider(); + var handler = await provider.ResolveAsync(services.BuildServiceProvider()); + Assert.IsFalse(handler); + tenantSetter.Verify(setter => setter.SetTenant(It.IsAny()), Times.Never); + } + + [TestMethod] + public async Task TestHttpContextItemTenantParser3Async() + { + var services = new ServiceCollection(); + string tenantKey = "tenant"; + Mock tenantSetter = new(); + tenantSetter.Setup(setter => setter.SetTenant(It.IsAny())).Verifiable(); + services.AddScoped(_ => tenantSetter.Object); + services.Configure(option => + { + option.TenantKey = tenantKey; + }); + var provider = new HttpContextItemTenantParserProvider(); + var handler = await provider.ResolveAsync(services.BuildServiceProvider()); + Assert.IsFalse(handler); + tenantSetter.Verify(setter => setter.SetTenant(It.IsAny()), Times.Never); + } + + [TestMethod] + public async Task TestQueryStringTenantParserAsync() + { + var services = new ServiceCollection(); + services.AddHttpContextAccessor(); + string tenantKey = "tenant"; + Mock tenantSetter = new(); + tenantSetter.Setup(setter => setter.SetTenant(It.IsAny())).Verifiable(); + services.AddScoped(_ => tenantSetter.Object); + services.Configure(option => + { + option.TenantKey = tenantKey; + }); + var serviceProvider = services.BuildServiceProvider(); + var httpContextAccessor = serviceProvider.GetRequiredService(); + httpContextAccessor.HttpContext = new DefaultHttpContext() + { + Request = { QueryString = QueryString.Create(tenantKey, "1") } + }; + var provider = new QueryStringTenantParserProvider(); + Assert.IsTrue(provider.Name == "QueryString"); + var handler = await provider.ResolveAsync(services.BuildServiceProvider()); + Assert.IsTrue(handler); + tenantSetter.Verify(setter => setter.SetTenant(It.IsAny()), Times.Once); + } + + [TestMethod] + public async Task TestQueryStringTenantParser2Async() + { + var services = new ServiceCollection(); + services.AddHttpContextAccessor(); + string tenantKey = "tenant"; + Mock tenantSetter = new(); + tenantSetter.Setup(setter => setter.SetTenant(It.IsAny())).Verifiable(); + services.AddScoped(_ => tenantSetter.Object); + services.Configure(option => + { + option.TenantKey = tenantKey; + }); + var serviceProvider = services.BuildServiceProvider(); + var httpContextAccessor = serviceProvider.GetRequiredService(); + httpContextAccessor.HttpContext = new DefaultHttpContext() + { + Request = { QueryString = new QueryString() } + }; + var provider = new QueryStringTenantParserProvider(); + var handler = await provider.ResolveAsync(services.BuildServiceProvider()); + Assert.IsFalse(handler); + tenantSetter.Verify(setter => setter.SetTenant(It.IsAny()), Times.Never); + } + + [TestMethod] + public async Task TestQueryStringTenantParser3Async() + { + var services = new ServiceCollection(); + string tenantKey = "tenant"; + Mock tenantSetter = new(); + tenantSetter.Setup(setter => setter.SetTenant(It.IsAny())).Verifiable(); + services.AddScoped(_ => tenantSetter.Object); + services.Configure(option => + { + option.TenantKey = tenantKey; + }); + var provider = new QueryStringTenantParserProvider(); + var handler = await provider.ResolveAsync(services.BuildServiceProvider()); + Assert.IsFalse(handler); + tenantSetter.Verify(setter => setter.SetTenant(It.IsAny()), Times.Never); + } + + [TestMethod] + public async Task TestRouteTenantParserAsync() + { + var services = new ServiceCollection(); + services.AddHttpContextAccessor(); + string tenantKey = "tenant"; + Mock tenantSetter = new(); + tenantSetter.Setup(setter => setter.SetTenant(It.IsAny())).Verifiable(); + services.AddScoped(_ => tenantSetter.Object); + services.Configure(option => + { + option.TenantKey = tenantKey; + }); + var serviceProvider = services.BuildServiceProvider(); + var httpContextAccessor = serviceProvider.GetRequiredService(); + httpContextAccessor.HttpContext = new DefaultHttpContext() + { + Request = + { + RouteValues = new RouteValueDictionary() + { + { tenantKey, "1" } + } + } + }; + var provider = new RouteTenantParserProvider(); + Assert.IsTrue(provider.Name == "Route"); + var handler = await provider.ResolveAsync(services.BuildServiceProvider()); + Assert.IsTrue(handler); + tenantSetter.Verify(setter => setter.SetTenant(It.IsAny()), Times.Once); + } + + [TestMethod] + public async Task TestRouteTenantParser2Async() + { + var services = new ServiceCollection(); + services.AddHttpContextAccessor(); + string tenantKey = "tenant"; + Mock tenantSetter = new(); + tenantSetter.Setup(setter => setter.SetTenant(It.IsAny())).Verifiable(); + services.AddScoped(_ => tenantSetter.Object); + services.Configure(option => + { + option.TenantKey = tenantKey; + }); + var serviceProvider = services.BuildServiceProvider(); + var httpContextAccessor = serviceProvider.GetRequiredService(); + httpContextAccessor.HttpContext = new DefaultHttpContext() + { + Request = + { + RouteValues = new RouteValueDictionary() + } + }; + var provider = new RouteTenantParserProvider(); + var handler = await provider.ResolveAsync(services.BuildServiceProvider()); + Assert.IsFalse(handler); + tenantSetter.Verify(setter => setter.SetTenant(It.IsAny()), Times.Never); + } + + [TestMethod] + public async Task TestRouteTenantParser3Async() + { + var services = new ServiceCollection(); + string tenantKey = "tenant"; + Mock tenantSetter = new(); + tenantSetter.Setup(setter => setter.SetTenant(It.IsAny())).Verifiable(); + services.AddScoped(_ => tenantSetter.Object); + services.Configure(option => + { + option.TenantKey = tenantKey; + }); + var provider = new RouteTenantParserProvider(); + var handler = await provider.ResolveAsync(services.BuildServiceProvider()); + Assert.IsFalse(handler); + tenantSetter.Verify(setter => setter.SetTenant(It.IsAny()), Times.Never); + } + + [TestMethod] + public async Task TestEnvironmentVariablesParserAsync() + { + var services = new ServiceCollection(); + Mock environmentSetter = new(); + string environmentKey = "env"; + environmentSetter.Setup(setter => setter.SetEnvironment(It.IsAny())).Verifiable(); + services.AddScoped(_ => environmentSetter.Object); + services.Configure(option => + { + option.EnvironmentKey = environmentKey; + }); + System.Environment.SetEnvironmentVariable(environmentKey, "dev"); + var serviceProvider = services.BuildServiceProvider(); + var environmentVariablesParserProvider = new EnvironmentVariablesParserProvider(); + var handler = await environmentVariablesParserProvider.ResolveAsync(serviceProvider); + Assert.IsTrue(handler); + } + + [TestMethod] + public async Task TestEnvironmentVariablesParser2Async() + { + var services = new ServiceCollection(); + Mock environmentSetter = new(); + string environmentKey = "env"; + System.Environment.SetEnvironmentVariable(environmentKey, ""); + environmentSetter.Setup(setter => setter.SetEnvironment(It.IsAny())).Verifiable(); + services.AddScoped(_ => environmentSetter.Object); + services.Configure(option => + { + option.EnvironmentKey = environmentKey; + }); + var serviceProvider = services.BuildServiceProvider(); + var environmentVariablesParserProvider = new EnvironmentVariablesParserProvider(); + var handler = await environmentVariablesParserProvider.ResolveAsync(serviceProvider); + Assert.IsFalse(handler); + } + + [TestMethod] + public void TestGetDbContextOptionsList() + { + var services = new ServiceCollection(); + services.Configure(option => + { + option.DefaultConnection = "data source=test2"; + option.Isolations = new() + { + new() + { + Environment = "dev", + ConnectionString = "data source=test3" + }, + new() + { + Environment = "pro", + ConnectionString = "data source=test4" + } + }; + }); + services.AddSingleton(); + var serviceProvider = services.BuildServiceProvider(); + var provider = serviceProvider.GetRequiredService(); + Assert.IsTrue(provider.DbContextOptionsList.Distinct().Count() == 3); + } + + private string GetDataBaseConnectionString(CustomDbContext dbContext) => dbContext.Database.GetConnectionString()!; + +} diff --git a/test/Masa.Contrib.Isolation.UoW.EF.Tests/_Imports.cs b/test/Masa.Contrib.Isolation.UoW.EF.Tests/_Imports.cs new file mode 100644 index 000000000..345f540e1 --- /dev/null +++ b/test/Masa.Contrib.Isolation.UoW.EF.Tests/_Imports.cs @@ -0,0 +1,24 @@ +global using Masa.BuildingBlocks.Data.UoW; +global using Masa.BuildingBlocks.Dispatcher.Events; +global using Masa.BuildingBlocks.Isolation; +global using Masa.BuildingBlocks.Isolation.Environment; +global using Masa.BuildingBlocks.Isolation.MultiTenant; +global using Masa.BuildingBlocks.Isolation.Options; +global using Masa.Contrib.Isolation.Environment; +global using Masa.Contrib.Isolation.UoW.EF.Parser.Environment; +global using Masa.Utils.Data.EntityFrameworkCore; +global using Masa.Utils.Data.EntityFrameworkCore.Sqlite; +global using Microsoft.AspNetCore.Http; +global using Microsoft.AspNetCore.Routing; +global using Microsoft.Data.Sqlite; +global using Microsoft.EntityFrameworkCore; +global using Microsoft.EntityFrameworkCore.Metadata.Builders; +global using Microsoft.Extensions.Configuration; +global using Microsoft.Extensions.DependencyInjection; +global using Microsoft.Extensions.Primitives; +global using Microsoft.VisualStudio.TestTools.UnitTesting; +global using Moq; +global using System; +global using System.IO; +global using System.Linq; + diff --git a/test/Masa.Contrib.Isolation.UoW.EF.Tests/appsettings.json b/test/Masa.Contrib.Isolation.UoW.EF.Tests/appsettings.json new file mode 100644 index 000000000..d60224647 --- /dev/null +++ b/test/Masa.Contrib.Isolation.UoW.EF.Tests/appsettings.json @@ -0,0 +1,18 @@ +{ + "ConnectionStrings": { + "DefaultConnection": "data source=test1", + "Isolations": [ + { + "TenantId": "*", + "Environment": "development", + "ConnectionString": "data source=test2", + "Score": 99 + }, + { + "TenantId": "00000000-0000-0000-0000-000000000002", + "Environment": "production", + "ConnectionString": "data source=test3" + } + ] + } +} \ No newline at end of file diff --git a/test/Masa.Contrib.Isolation.UoW.EF.Web.Tests/CustomDbContext.cs b/test/Masa.Contrib.Isolation.UoW.EF.Web.Tests/CustomDbContext.cs new file mode 100644 index 000000000..75150cd4c --- /dev/null +++ b/test/Masa.Contrib.Isolation.UoW.EF.Web.Tests/CustomDbContext.cs @@ -0,0 +1,49 @@ +namespace Masa.Contrib.Isolation.UoW.EF.Web.Tests; + +public class CustomDbContext : IsolationDbContext +{ + public CustomDbContext(MasaDbContextOptions options) : base(options) { } + + public DbSet User { get; set; } + + protected override void OnModelCreatingExecuting(ModelBuilder builder) + { + builder.Entity(ConfigureUserEntry); + } + + void ConfigureUserEntry(EntityTypeBuilder builder) + { + builder.ToTable("Users"); + + builder.HasKey(e => e.Id); + + builder.Property(e => e.Id) + .IsRequired(); + + builder.Property(e => e.Account) + .HasMaxLength(20) + .IsRequired(); + + builder.Property(e => e.Account) + .HasMaxLength(50) + .IsRequired(); + } +} + +public class Users : IIsolation +{ + public Guid Id { get; private set; } + + public string Account { get; set; } = default!; + + public string Password { get; set; } = default!; + + public Users() + { + this.Id = Guid.NewGuid(); + } + + public int TenantId { get; set; } + + public string Environment { get; set; } +} diff --git a/test/Masa.Contrib.Isolation.UoW.EF.Web.Tests/EdgeDriverTest.cs b/test/Masa.Contrib.Isolation.UoW.EF.Web.Tests/EdgeDriverTest.cs new file mode 100644 index 000000000..921337ae9 --- /dev/null +++ b/test/Masa.Contrib.Isolation.UoW.EF.Web.Tests/EdgeDriverTest.cs @@ -0,0 +1,42 @@ +namespace Masa.Contrib.Isolation.UoW.EF.Web.Tests; + +[TestClass] +public class EdgeDriverTest +{ + private IServiceCollection _services; + + [TestInitialize] + public void Initialize() + { + var configurationRoot = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json", true, true) + .Build(); + _services = new ServiceCollection(); + _services.AddSingleton(configurationRoot); + _services.AddEventBus(eventBusBuilder => eventBusBuilder.UseIsolationUoW(dbOptions => dbOptions.UseSqlite(), + isolationBuilder => isolationBuilder.SetTenantKey("tenant").SetEnvironmentKey("env").UseMultiTenant().UseEnvironment())); + System.Environment.SetEnvironmentVariable("env", "pro"); + } + + [TestMethod] + public async Task TestTenantAsync() + { + var serviceProvider = _services.BuildServiceProvider(); + + #region Manually assign values to tenants and environments, and in real scenarios, automatically parse and assign values based on the current HttpContext + + var httpContextAccessor = serviceProvider.GetRequiredService(); + httpContextAccessor.HttpContext = new DefaultHttpContext(); + httpContextAccessor.HttpContext.Items = new Dictionary() + { + { "tenant", "2" } + }; + + #endregion + + var registerUserEvent = new RegisterUserEvent("jim", "123456"); + var eventBus = serviceProvider.GetRequiredService(); + await eventBus.PublishAsync(registerUserEvent); + } +} diff --git a/test/Masa.Contrib.Isolation.UoW.EF.Web.Tests/EventHandlers/RegisterUserEventHandler.cs b/test/Masa.Contrib.Isolation.UoW.EF.Web.Tests/EventHandlers/RegisterUserEventHandler.cs new file mode 100644 index 000000000..f788d9939 --- /dev/null +++ b/test/Masa.Contrib.Isolation.UoW.EF.Web.Tests/EventHandlers/RegisterUserEventHandler.cs @@ -0,0 +1,51 @@ +using Masa.BuildingBlocks.Isolation.Environment; + +namespace Masa.Contrib.Isolation.UoW.EF.Web.Tests.EventHandlers; + +public class RegisterUserEventHandler +{ + private readonly CustomDbContext _customDbContext; + private readonly IDataFilter _dataFilter; + private readonly IEnvironmentSetter _environmentSetter; + private readonly IEnvironmentContext _environmentContext; + + public RegisterUserEventHandler(CustomDbContext customDbContext, IDataFilter dataFilter, IEnvironmentSetter environmentSetter, + IEnvironmentContext environmentContext) + { + _customDbContext = customDbContext; + _dataFilter = dataFilter; + _environmentSetter = environmentSetter; + _environmentContext = environmentContext; + } + + [EventHandler] + public async Task RegisterUserAsync(RegisterUserEvent @event) + { + await _customDbContext.Database.EnsureCreatedAsync(); + Assert.IsTrue(_customDbContext.Database.GetConnectionString() == "data source=test3"); + var user = new Users() + { + Account = @event.Account, + Password = MD5Utils.Encrypt(@event.Password, @event.Password) + }; + await _customDbContext.Set().AddAsync(user); + await _customDbContext.SaveChangesAsync(); + + var user2 = await _customDbContext.Set().FirstOrDefaultAsync(); + Assert.IsTrue(user2!.Account == @event.Account); + Assert.IsTrue(user2.Environment == "pro"); + Assert.IsTrue(user2.TenantId == 2); + + _environmentSetter.SetEnvironment("dev"); //In EventHandler, physical isolation is not retriggered if a new DbContext is not recreated, it can only be used to filter changes + Assert.IsTrue(_environmentContext.CurrentEnvironment == "dev"); + + var user3 = await _customDbContext.Set().FirstOrDefaultAsync(); + Assert.IsNull(user3); + + using (_dataFilter.Disable()) + { + var user4 = await _customDbContext.Set().FirstOrDefaultAsync(); + Assert.IsNotNull(user4); + } + } +} diff --git a/test/Masa.Contrib.Isolation.UoW.EF.Web.Tests/Events/RegisterUserEvent.cs b/test/Masa.Contrib.Isolation.UoW.EF.Web.Tests/Events/RegisterUserEvent.cs new file mode 100644 index 000000000..c01932464 --- /dev/null +++ b/test/Masa.Contrib.Isolation.UoW.EF.Web.Tests/Events/RegisterUserEvent.cs @@ -0,0 +1,5 @@ +namespace Masa.Contrib.Isolation.UoW.EF.Web.Tests.Events; + +public record RegisterUserEvent(string Account,string Password) : Event +{ +} diff --git a/test/Masa.Contrib.Isolation.UoW.EF.Web.Tests/Masa.Contrib.Isolation.UoW.EF.Web.Tests.csproj b/test/Masa.Contrib.Isolation.UoW.EF.Web.Tests/Masa.Contrib.Isolation.UoW.EF.Web.Tests.csproj new file mode 100644 index 000000000..8293b0dce --- /dev/null +++ b/test/Masa.Contrib.Isolation.UoW.EF.Web.Tests/Masa.Contrib.Isolation.UoW.EF.Web.Tests.csproj @@ -0,0 +1,32 @@ + + + + net6.0 + enable + false + enable + + + + + + + + + + + + + + + + + + + + + Always + + + + diff --git a/test/Masa.Contrib.Isolation.UoW.EF.Web.Tests/TestBase.cs b/test/Masa.Contrib.Isolation.UoW.EF.Web.Tests/TestBase.cs new file mode 100644 index 000000000..53d30e7b3 --- /dev/null +++ b/test/Masa.Contrib.Isolation.UoW.EF.Web.Tests/TestBase.cs @@ -0,0 +1,18 @@ +namespace Masa.Contrib.Isolation.UoW.EF.Web.Tests; + +public class TestBase : IDisposable +{ + protected readonly string _connectionString = "DataSource=:memory:"; + protected readonly SqliteConnection Connection; + + protected TestBase() + { + Connection = new SqliteConnection(_connectionString); + Connection.Open(); + } + + public void Dispose() + { + Connection.Close(); + } +} diff --git a/test/Masa.Contrib.Isolation.UoW.EF.Web.Tests/_Imports.cs b/test/Masa.Contrib.Isolation.UoW.EF.Web.Tests/_Imports.cs new file mode 100644 index 000000000..df58f49a4 --- /dev/null +++ b/test/Masa.Contrib.Isolation.UoW.EF.Web.Tests/_Imports.cs @@ -0,0 +1,20 @@ +global using Masa.BuildingBlocks.Dispatcher.Events; +global using Masa.BuildingBlocks.Isolation; +global using Masa.Contrib.Dispatcher.Events; +global using Masa.Contrib.Isolation.Environment; +global using Masa.Contrib.Isolation.MultiTenant; +global using Masa.Contrib.Isolation.UoW.EF.Web.Tests.Events; +global using Masa.Utils.Data.EntityFrameworkCore; +global using Masa.Utils.Data.EntityFrameworkCore.Filters; +global using Masa.Utils.Data.EntityFrameworkCore.Sqlite; +global using Masa.Utils.Security.Cryptography; +global using Microsoft.AspNetCore.Http; +global using Microsoft.Data.Sqlite; +global using Microsoft.EntityFrameworkCore; +global using Microsoft.EntityFrameworkCore.Metadata.Builders; +global using Microsoft.Extensions.Configuration; +global using Microsoft.Extensions.DependencyInjection; +global using Microsoft.VisualStudio.TestTools.UnitTesting; +global using System; +global using System.IO; +global using System.Threading.Tasks; diff --git a/test/Masa.Contrib.Isolation.UoW.EF.Web.Tests/appsettings.json b/test/Masa.Contrib.Isolation.UoW.EF.Web.Tests/appsettings.json new file mode 100644 index 000000000..460845f83 --- /dev/null +++ b/test/Masa.Contrib.Isolation.UoW.EF.Web.Tests/appsettings.json @@ -0,0 +1,18 @@ +{ + "ConnectionStrings": { + "DefaultConnection": "data source=test1", + "Isolations": [ + { + "TenantId": "1", + "Environment": "dev", + "ConnectionString": "data source=test2", + "Score": 99 + }, + { + "TenantId": "2", + "Environment": "pro", + "ConnectionString": "data source=test3" + } + ] + } +} \ No newline at end of file