From 3f0ed4b8848744749b189ea8c6e216307588cb32 Mon Sep 17 00:00:00 2001 From: Jon P Smith Date: Wed, 16 Mar 2022 11:09:54 +0000 Subject: [PATCH] TenantTypes is now [Flags] and has AddSharding --- AuthPermissions.AspNetCore/SetupExtensions.cs | 6 +- .../Services/AuthRolesAdminService.cs | 2 +- .../Services/AuthTenantAdminService.cs | 13 ++-- .../Services/AuthUsersAdminService.cs | 3 +- .../Services/Internal/ChangeRoleTypeChecks.cs | 3 - .../AdminCode/TenantTypeExtensions.cs | 44 ++++++++++++++ .../Concrete/BulkLoadTenantsService.cs | 6 +- .../Concrete/BulkLoadUsersService.cs | 4 +- AuthPermissions/ClaimsCalculator.cs | 6 +- .../SetupCode/BulkLoadOnStartup.cs | 4 +- AuthPermissions/SetupCode/TenantTypes.cs | 21 +++++-- AuthPermissions/SetupExtensions.cs | 2 + ReleaseNotes.md | 2 + Test/TestData/Test.txt | 2 +- .../TestAspNetSetupExtension.cs | 51 +++++++++++++++- UpdateToVersion3.md | 59 +++++++++++++++++++ 16 files changed, 201 insertions(+), 27 deletions(-) create mode 100644 AuthPermissions/AdminCode/TenantTypeExtensions.cs diff --git a/AuthPermissions.AspNetCore/SetupExtensions.cs b/AuthPermissions.AspNetCore/SetupExtensions.cs index 90dfa90a..03832a4c 100644 --- a/AuthPermissions.AspNetCore/SetupExtensions.cs +++ b/AuthPermissions.AspNetCore/SetupExtensions.cs @@ -199,7 +199,7 @@ private static void RegisterCommonServices(this AuthSetupData setupData) setupData.Services.AddScoped(); setupData.Services.AddTransient(); setupData.Services.AddTransient(); - if (setupData.Options.TenantType != TenantTypes.NotUsingTenants) + if (setupData.Options.TenantType.IsMultiTenant()) SetupMultiTenantServices(setupData); //The factories for the optional services @@ -231,15 +231,13 @@ private static void SetupMultiTenantServices(AuthSetupData setupData) { //This sets up the code to get the DataKey to the application's DbContext - - if (setupData.Options.LinkToTenantType == LinkToTenantTypes.NotTurnedOn) //This uses the efficient GetDataKey from user setupData.Services.AddScoped(); else { //Check the TenantType and LinkToTenantType for incorrect versions - if (setupData.Options.TenantType != TenantTypes.SingleLevel + if (!setupData.Options.TenantType.IsHierarchical() && setupData.Options.LinkToTenantType == LinkToTenantTypes.AppAndHierarchicalUsers) throw new AuthPermissionsException( $"You can't set the {nameof(AuthPermissionsOptions.LinkToTenantType)} to " + diff --git a/AuthPermissions/AdminCode/Services/AuthRolesAdminService.cs b/AuthPermissions/AdminCode/Services/AuthRolesAdminService.cs index 3bd1ff43..5e975952 100644 --- a/AuthPermissions/AdminCode/Services/AuthRolesAdminService.cs +++ b/AuthPermissions/AdminCode/Services/AuthRolesAdminService.cs @@ -36,7 +36,7 @@ public AuthRolesAdminService(AuthPermissionsDbContext context, AuthPermissionsOp { _context = context; _permissionType = options.InternalData.EnumPermissionsType; - _isMultiTenant = options.TenantType != TenantTypes.NotUsingTenants; + _isMultiTenant = options.TenantType.IsMultiTenant(); } /// diff --git a/AuthPermissions/AdminCode/Services/AuthTenantAdminService.cs b/AuthPermissions/AdminCode/Services/AuthTenantAdminService.cs index 3069b2e7..d66b67c5 100644 --- a/AuthPermissions/AdminCode/Services/AuthTenantAdminService.cs +++ b/AuthPermissions/AdminCode/Services/AuthTenantAdminService.cs @@ -6,6 +6,7 @@ using System.Data; using System.Linq; using System.Threading.Tasks; +using AuthPermissions.AdminCode.Services.Internal; using AuthPermissions.CommonCode; using AuthPermissions.DataLayer; using AuthPermissions.DataLayer.Classes; @@ -71,7 +72,7 @@ public IQueryable QueryTenants() /// query on the AuthP database public IQueryable QueryEndLeafTenants() { - return _tenantType == TenantTypes.SingleLevel + return _tenantType.IsSingleLevel() ? QueryTenants() : _context.Tenants.Where(x => !x.Children.Any()); } @@ -138,9 +139,9 @@ public async Task AddSingleTenantAsync(string tenantName, List AddHierarchicalTenantAsync(string tenantName, { var status = new StatusGenericHandler(); - if (_tenantType != TenantTypes.HierarchicalTenant) + if (!_tenantType.IsHierarchical()) throw new AuthPermissionsException( $"You must set the {nameof(AuthPermissionsOptions.TenantType)} before you can use tenants"); if (tenantName.Contains('|')) @@ -258,7 +259,7 @@ public async Task AddHierarchicalTenantAsync(string tenantName, /// public async Task UpdateTenantRolesAsync(int tenantId, List newTenantRoleNames) { - if (_tenantType == TenantTypes.NotUsingTenants) + if (!_tenantType.IsMultiTenant()) throw new AuthPermissionsException( $"You must set the {nameof(AuthPermissionsOptions.TenantType)} parameter in the AuthP's options"); @@ -377,7 +378,7 @@ public async Task MoveHierarchicalTenantToAnotherParentAsync(int { var status = new StatusGenericHandler { }; - if (_tenantType != TenantTypes.HierarchicalTenant) + if (!_tenantType.IsHierarchical()) throw new AuthPermissionsException( $"You cannot add a hierarchical tenant because the tenant configuration is {_tenantType}"); diff --git a/AuthPermissions/AdminCode/Services/AuthUsersAdminService.cs b/AuthPermissions/AdminCode/Services/AuthUsersAdminService.cs index c5fcb8cf..d162d124 100644 --- a/AuthPermissions/AdminCode/Services/AuthUsersAdminService.cs +++ b/AuthPermissions/AdminCode/Services/AuthUsersAdminService.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using AuthPermissions.AdminCode.Services.Internal; using AuthPermissions.CommonCode; using AuthPermissions.DataLayer.Classes; using AuthPermissions.DataLayer.Classes.SupportTypes; @@ -37,7 +38,7 @@ public AuthUsersAdminService(AuthPermissionsDbContext context, { _context = context ?? throw new ArgumentNullException(nameof(context)); _syncAuthenticationUsersFactory = syncAuthenticationUsersFactory; - _isMultiTenant = options.TenantType != TenantTypes.NotUsingTenants; + _isMultiTenant = options.TenantType.IsMultiTenant(); } /// diff --git a/AuthPermissions/AdminCode/Services/Internal/ChangeRoleTypeChecks.cs b/AuthPermissions/AdminCode/Services/Internal/ChangeRoleTypeChecks.cs index a135500a..50ee91f3 100644 --- a/AuthPermissions/AdminCode/Services/Internal/ChangeRoleTypeChecks.cs +++ b/AuthPermissions/AdminCode/Services/Internal/ChangeRoleTypeChecks.cs @@ -1,10 +1,7 @@ // Copyright (c) 2022 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ // Licensed under MIT license. See License.txt in the project root for license information. -using System; -using System.Collections.Generic; using System.Linq; -using System.Security.Cryptography.X509Certificates; using System.Threading.Tasks; using AuthPermissions.CommonCode; using AuthPermissions.DataLayer.Classes.SupportTypes; diff --git a/AuthPermissions/AdminCode/TenantTypeExtensions.cs b/AuthPermissions/AdminCode/TenantTypeExtensions.cs new file mode 100644 index 00000000..6cc9e4c7 --- /dev/null +++ b/AuthPermissions/AdminCode/TenantTypeExtensions.cs @@ -0,0 +1,44 @@ +// Copyright (c) 2022 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ +// Licensed under MIT license. See License.txt in the project root for license information. + +using AuthPermissions.CommonCode; +using AuthPermissions.SetupCode; + +namespace AuthPermissions.AdminCode; + +public static class TenantTypeExtensions +{ + public static void ThrowExceptionIfTenantTypeIsWrong(this TenantTypes tenantType) + { + if (tenantType.HasFlag(TenantTypes.SingleLevel) && tenantType.HasFlag(TenantTypes.HierarchicalTenant)) + throw new AuthPermissionsException( + $"The {nameof(AuthPermissionsOptions.TenantType)} option can't have {nameof(TenantTypes.SingleLevel)} and "+ + $"{nameof(TenantTypes.HierarchicalTenant)} at the same time."); + + if (!tenantType.IsMultiTenant() && tenantType.HasFlag(TenantTypes.AddSharding)) + throw new AuthPermissionsException( + $"You need to set the {nameof(AuthPermissionsOptions.TenantType)} option to either {nameof(TenantTypes.SingleLevel)} or " + + $"{nameof(TenantTypes.HierarchicalTenant)} when setting the {nameof(TenantTypes.AddSharding)} flag."); + + } + + public static bool IsMultiTenant(this TenantTypes tenantType) + { + return tenantType.HasFlag(TenantTypes.SingleLevel) || tenantType.HasFlag(TenantTypes.HierarchicalTenant); + } + + public static bool IsSingleLevel(this TenantTypes tenantType) + { + return tenantType.HasFlag(TenantTypes.SingleLevel); + } + + public static bool IsHierarchical(this TenantTypes tenantType) + { + return tenantType.HasFlag(TenantTypes.HierarchicalTenant); + } + + public static bool UsingSharding(this TenantTypes tenantType) + { + return tenantType.HasFlag(TenantTypes.AddSharding); + } +} \ No newline at end of file diff --git a/AuthPermissions/BulkLoadServices/Concrete/BulkLoadTenantsService.cs b/AuthPermissions/BulkLoadServices/Concrete/BulkLoadTenantsService.cs index ac9d6daa..3cd7eb97 100644 --- a/AuthPermissions/BulkLoadServices/Concrete/BulkLoadTenantsService.cs +++ b/AuthPermissions/BulkLoadServices/Concrete/BulkLoadTenantsService.cs @@ -5,6 +5,8 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using AuthPermissions.AdminCode; +using AuthPermissions.AdminCode.Services.Internal; using AuthPermissions.DataLayer.Classes; using AuthPermissions.DataLayer.Classes.SupportTypes; using AuthPermissions.DataLayer.EfCode; @@ -52,14 +54,14 @@ public async Task AddTenantsToDatabaseAsync(List x.TenantName) .GroupBy(x => x).Where(x => x.Count() > 1).Select(x => x.Key).ToList(); diff --git a/AuthPermissions/BulkLoadServices/Concrete/BulkLoadUsersService.cs b/AuthPermissions/BulkLoadServices/Concrete/BulkLoadUsersService.cs index 3d049c9f..06de4869 100644 --- a/AuthPermissions/BulkLoadServices/Concrete/BulkLoadUsersService.cs +++ b/AuthPermissions/BulkLoadServices/Concrete/BulkLoadUsersService.cs @@ -5,6 +5,8 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using AuthPermissions.AdminCode; +using AuthPermissions.AdminCode.Services.Internal; using AuthPermissions.BulkLoadServices.Concrete.Internal; using AuthPermissions.CommonCode; using AuthPermissions.DataLayer.Classes; @@ -108,7 +110,7 @@ private async Task CreateUserTenantAndAddToDbAsync(BulkLoadUserW (findUserInfoService == null ? " wasn't available." : " couldn't find it either."))); Tenant userTenant = null; - if (_options.TenantType != TenantTypes.NotUsingTenants && !string.IsNullOrEmpty(userDefine.TenantNameForDataKey)) + if (_options.TenantType.IsMultiTenant() && !string.IsNullOrEmpty(userDefine.TenantNameForDataKey)) { userTenant = await _context.Tenants.SingleOrDefaultAsync(x => x.TenantFullName == userDefine.TenantNameForDataKey); if (userTenant == null) diff --git a/AuthPermissions/ClaimsCalculator.cs b/AuthPermissions/ClaimsCalculator.cs index 3ff99fb1..219f02e9 100644 --- a/AuthPermissions/ClaimsCalculator.cs +++ b/AuthPermissions/ClaimsCalculator.cs @@ -5,6 +5,8 @@ using System.Linq; using System.Security.Claims; using System.Threading.Tasks; +using AuthPermissions.AdminCode; +using AuthPermissions.AdminCode.Services.Internal; using AuthPermissions.DataLayer.Classes.SupportTypes; using AuthPermissions.DataLayer.EfCode; using AuthPermissions.PermissionsCode; @@ -82,7 +84,7 @@ private async Task CalcPermissionsForUserAsync(string userId) .Select(x => x.Role.PackedPermissionsInRole) .ToListAsync(); - if (_options.TenantType != TenantTypes.NotUsingTenants) + if (_options.TenantType.IsMultiTenant()) { //We need to add any RoleTypes.TenantAdminAdd for a tenant user @@ -113,7 +115,7 @@ private async Task CalcPermissionsForUserAsync(string userId) /// Returns the dataKey, or null if a) tenant isn't turned on, or b) the user doesn't have a tenant private async Task GetDataKeyAsync(string userid) { - if (_options.TenantType == TenantTypes.NotUsingTenants) + if (!_options.TenantType.IsMultiTenant()) return null; var userWithTenant = await _context.AuthUsers.Include(x => x.UserTenant) diff --git a/AuthPermissions/SetupCode/BulkLoadOnStartup.cs b/AuthPermissions/SetupCode/BulkLoadOnStartup.cs index 1139f9ea..f6d5f2a0 100644 --- a/AuthPermissions/SetupCode/BulkLoadOnStartup.cs +++ b/AuthPermissions/SetupCode/BulkLoadOnStartup.cs @@ -4,6 +4,8 @@ using System; using System.Linq; using System.Threading.Tasks; +using AuthPermissions.AdminCode; +using AuthPermissions.AdminCode.Services.Internal; using AuthPermissions.BulkLoadServices.Concrete; using AuthPermissions.DataLayer.EfCode; using AuthPermissions.SetupCode.Factories; @@ -37,7 +39,7 @@ public static async Task SeedRolesTenantsUsersIfEmpty(this AuthP status = await roleLoader.AddRolesToDatabaseAsync(options.InternalData.RolesPermissionsSetupData); } - if (status is { IsValid: true } && options.TenantType != TenantTypes.NotUsingTenants && !context.Tenants.Any()) + if (status is { IsValid: true } && options.TenantType.IsMultiTenant() && !context.Tenants.Any()) { var tenantLoader = new BulkLoadTenantsService(context); status = await tenantLoader.AddTenantsToDatabaseAsync(options.InternalData.TenantSetupData, options); diff --git a/AuthPermissions/SetupCode/TenantTypes.cs b/AuthPermissions/SetupCode/TenantTypes.cs index 6118ab1f..fdf1687b 100644 --- a/AuthPermissions/SetupCode/TenantTypes.cs +++ b/AuthPermissions/SetupCode/TenantTypes.cs @@ -1,26 +1,39 @@ // Copyright (c) 2021 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ // Licensed under MIT license. See License.txt in the project root for license information. +using System; + namespace AuthPermissions.SetupCode { /// - /// This defines the types of tenant the AuthPermissions can handle + /// This defines the types of tenant the AuthPermissions can handle, with optional sharding /// + [Flags] public enum TenantTypes { /// /// Usage of tenants are turned off /// - NotUsingTenants, + NotUsingTenants = 0, /// /// Multi-tenant with one level only, e.g. a company has different departments: sales, finance, HR etc. /// A User can only be in one of these levels /// - SingleLevel, + SingleLevel = 1, + /// + /// Multi-tenant with one level only, e.g. a company has different departments: sales, finance, HR etc. + /// A tenant can be mixed in with + /// /// /// Multi-tenant many levels, e.g. Holding company -> USA branch -> East Coast -> New York /// A User at the USA branch has read/write access to the USA branch data, read-only access to the East Coast and all its subsidiaries /// - HierarchicalTenant + HierarchicalTenant = 2, + /// + /// This turns on the sharding. Sharding allows tenants to be split across many databases, including placing a tenant's data in its own database. + /// + /// + AddSharding = 4 + } } \ No newline at end of file diff --git a/AuthPermissions/SetupExtensions.cs b/AuthPermissions/SetupExtensions.cs index 0184a659..ce9c5385 100644 --- a/AuthPermissions/SetupExtensions.cs +++ b/AuthPermissions/SetupExtensions.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using AuthPermissions.AdminCode; using AuthPermissions.CommonCode; using AuthPermissions.DataLayer.Classes.SupportTypes; using AuthPermissions.DataLayer.EfCode; @@ -32,6 +33,7 @@ public static AuthSetupData RegisterAuthPermissions(this IServ { var authOptions = new AuthPermissionsOptions(); options?.Invoke(authOptions); + authOptions.TenantType.ThrowExceptionIfTenantTypeIsWrong(); authOptions.InternalData.EnumPermissionsType = typeof(TEnumPermissions); authOptions.InternalData.EnumPermissionsType.ThrowExceptionIfEnumIsNotCorrect(); authOptions.InternalData.EnumPermissionsType.ThrowExceptionIfEnumHasMembersHaveDuplicateValues(); diff --git a/ReleaseNotes.md b/ReleaseNotes.md index 28d9dacb..2cb17958 100644 --- a/ReleaseNotes.md +++ b/ReleaseNotes.md @@ -4,6 +4,8 @@ - BREAKING CHANGE: The ITenantChangeService has changed to allow multi-tenant sharding to be added - see #14 - BREAKING CHANGE: The option called AppConnectionString has been removed. Its longer needed because of ITenantChangeService change +- New Feature: Adding optional sharding to either a single-level or hierarchical multi-tenant applications - see documentation for an article explaining how to setup sharding + ## 2.3.1 diff --git a/Test/TestData/Test.txt b/Test/TestData/Test.txt index b090bcf8..15e68c26 100644 --- a/Test/TestData/Test.txt +++ b/Test/TestData/Test.txt @@ -1 +1 @@ -82e9ba63-1e53-4c0b-9a78-c3704baf399c \ No newline at end of file +8db6e054-0ea3-42f9-a143-4cba433737f7 \ No newline at end of file diff --git a/Test/UnitTests/TestAuthPermissionsAspNetCore/TestAspNetSetupExtension.cs b/Test/UnitTests/TestAuthPermissionsAspNetCore/TestAspNetSetupExtension.cs index 0346a90c..aa64a9bd 100644 --- a/Test/UnitTests/TestAuthPermissionsAspNetCore/TestAspNetSetupExtension.cs +++ b/Test/UnitTests/TestAuthPermissionsAspNetCore/TestAspNetSetupExtension.cs @@ -1,6 +1,7 @@ // Copyright (c) 2021 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ // Licensed under MIT license. See License.txt in the project root for license information. +using System; using System.Linq; using System.Threading.Tasks; using AuthPermissions; @@ -8,7 +9,7 @@ using AuthPermissions.AspNetCore; using AuthPermissions.AspNetCore.Services; using AuthPermissions.AspNetCore.StartupServices; -using AuthPermissions.DataLayer.EfCode; +using AuthPermissions.CommonCode; using AuthPermissions.SetupCode; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.DependencyInjection; @@ -32,6 +33,54 @@ public TestAspNetSetupExtension(ITestOutputHelper output) _output = output; } + private enum EnumNotShort {One, Two} + + [Fact] + public async Task TestRegisterAuthPermissionsEnumNotShort() + { + //SETUP + var services = this.SetupServicesForTest(); + + //ATTEMPT + var ex = await Assert.ThrowsAsync(async () => + await services.RegisterAuthPermissions() + .UsingInMemoryDatabase() + .SetupForUnitTestingAsync()); + + //VERIFY + ex.Message.ShouldStartWith($"The enum permissions {nameof(EnumNotShort)} should by 16 bits in size to work"); + } + + [Theory] + [InlineData(TenantTypes.NotUsingTenants, true)] + [InlineData(TenantTypes.SingleLevel, true)] + [InlineData(TenantTypes.HierarchicalTenant, true)] + [InlineData(TenantTypes.SingleLevel | TenantTypes.AddSharding, true)] + [InlineData(TenantTypes.HierarchicalTenant | TenantTypes.AddSharding, true)] + [InlineData(TenantTypes.SingleLevel | TenantTypes.HierarchicalTenant, false)] + [InlineData(TenantTypes.AddSharding, false)] + public async Task TestRegisterAuthPermissionsMultiTenantChecks(TenantTypes tenantType, bool success) + { + //SETUP + var services = this.SetupServicesForTest(); + + //ATTEMPT + try + { + await services.RegisterAuthPermissions(options => options.TenantType = tenantType) + .UsingInMemoryDatabase() + .SetupForUnitTestingAsync(); + } + catch (Exception e) + { + _output.WriteLine(e.Message); + success.ShouldBeFalse(); + return; + } + + //VERIFY + success.ShouldBeTrue(); + } [Fact] public void TestSetupAspNetCoreSetupAspNetCorePartHostedService() diff --git a/UpdateToVersion3.md b/UpdateToVersion3.md index e69de29b..2f0bfaa4 100644 --- a/UpdateToVersion3.md +++ b/UpdateToVersion3.md @@ -0,0 +1,59 @@ +# Migrating AuthPermissions.AspNetCore 2.* to 3.0 + +Version 3 of the AuthPermissions.AspNetCore library (shortened to **AuthP** from now on) contains a new sharding feature for multi-tenant applications. Please read the article called Using sharding to build multi-tenant apps using EF Core and ASP.NET Core] for a detailed explanation of sharding and how AuthP library provides a sharding implementation. + +This article explains how to update an existing AuthPermissions.AspNetCore 2.* project to AuthPermissions.AspNetCore 3.0. I assume that you are using Visual Studio. + +## TABLE OF CONTENT + +These are things you need to do to update aan application using AuthP version 2.0 to + +- **BRAKING CHANGES**: + - [Update your ITenantChangeService code] + + + +## BRAKING CHANGE: Update your ITenantChangeService code + +There is a significant change to the code your need to write to link the AuthP's tenant commands - create, update, delete and Move (hierarchal). Overall the code is simpler, but it does mean you need to update your code to handle your part of these commands in your application's DbContext. Here are the changes you need to change from version 2 to version 3 of the `ITenantChangeService`. See issue #15 for why this update was done. + +_NOTE: See Example3, [InvoiceTenantChangeService](https://github.com/JonPSmith/AuthPermissions.AspNetCore/blob/main/Example3.InvoiceCode/EfCoreCode/InvoiceTenantChangeService.cs), and Example4, [RetailTenantChangeService](https://github.com/JonPSmith/AuthPermissions.AspNetCore/blob/main/Example4.ShopCode/EfCoreCode/RetailTenantChangeService.cs), for examples of tenant change code for a single-level and hierarchical multi-tenant respectively._ + +### 1. Remove `GetNewInstanceOfAppContext` method and use dependency injection (DI) + +The new approach to obtaining a instance of your application's DbContext is to use DI injection via the class's constructor. This removes the need for the `GetNewInstanceOfAppContext` method. See the code below from Example3 shows how to inject the application's DbContext using DI - NOTE: The logger is optional - I use it to log any problems in my code. + +```c# +public class InvoiceTenantChangeService : ITenantChangeService +{ + private readonly InvoicesDbContext _context; + private readonly ILogger _logger; + + public InvoiceTenantChangeService( + InvoicesDbContext context, + ILogger logger) + { + _context = context; + _logger = logger; + } + + //The rest of the code is left out +``` + +This change makes your tenant change service more normal and removes the duplication of database setup code that was in the `GetNewInstanceOfAppContext` method. + +### 2. Use a transaction in methods that has multiple updates + +Previously the AuthP's tenant admin service created a transaction that covered both the update to the AuthP's DbContext and your application's DbContext. In the new design you have to add a transaction **if you have multiple, separate updates** to the database. This stops the possibility a partial update where of some updates have been applied, but an error happens during later updates. That could mean some data was lost. + +For instance, in Example3's tenant change service it uses a transaction in the Delete code because that code uses multiple `ExecuteSqlRawAsync` and a call to `SaveChangesAsync` - see the `SingleTenantDeleteAsync` in the [InvoiceTenantChangeService](https://github.com/JonPSmith/AuthPermissions.AspNetCore/blob/main/Example3.InvoiceCode/EfCoreCode/InvoiceTenantChangeService.cs) class. + +### 3. Changes to the names and parameters of the methods + +The changes are: + +- In all the methods the `appTransactionContext` is removed. In version 3 you use DI to get an instance of the your application's DbContext. +- The `HandleTenantDeleteAsync` has been split into + - `SingleTenantDeleteAsync` is called for a delete of a single-level tenant. + - `HierarchicalTenantDeleteAsync`is called for a delete of a hierarchical tenant. +- The new `HierarchicalTenantDeleteAsync` method takes in a list of tenants to be deleted (previously the method was called multiple times, but in version 3 you need to apply all the deletes within a transaction so that if a error happens then all the updates will be rolled back).