This article explains how to update an existing AuthPermissions.AspNetCore 1.* project to AuthPermissions.AspNetCore 2.0. I am assuming that you are using Visual Studio.
NOTE: I shorten the AuthPermissions.AspNetCore library name to AuthP from now on.
- MUST DO: Always
- MUST DO: If multi-tenant app
- OPTIONAL
To start, you need to download and install the correct NET 6 SDK for your development machine. You also need to download Visual Studio 2022, as Visual Studio 2019 doesn’t support Net 6.
You then have to change the target framework in every project's .csproj file that uses the AuthP library, ASP.NET Core or EF Core. i.e.
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
</PropertyGroup>
Once you have updated all your projects, then you can update all your NuGet packages to the latest version:
- AuthP should be 2.?.?
- ASP.NET Core and EF Core should be 6.?.?
NOTE: ASP.NET Core 6 has a new "minimal hosting" approach - read Andrew Lock's Upgrading a .NET 5 "Startup-based" app to .NET 6 article for your options.
AuthP version 2 uses the Net.RunMethodsSequentially library inside AuthP's SetupAspNetCoreAndDatabase
configuration method. This allow you to migrate / seed your database(s) on startup even if you are running multiple instances in production (see this article for more info).
However this new feature does require to changes to the registering code in your Net 5 Startup
code, and if you migrate / seed need your application's database on startup, then you will need to change how you do that.
The Net.RunMethodsSequentially library needs a global resource, such as a database, to lock against. But to handle the case of the database doesn't exist it needs a second global resource, which I have chosen as a FileSystem Directory, e.g. ASP.NET Core's wwwRoot directory.
You have to provide the database connection string and the FilePath to the ASP.NET Core's wwwRoot directory via the options part of the RegisterAuthPermissions
extension method - see the setup code below, which was taken from Example5's Startup class.
public void ConfigureServices(IServiceCollection services)
{
var connectionString = _configuration.GetConnectionString("DefaultConnection");
services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(_configuration.GetSection("AzureAd"));
services.AddControllersWithViews();
services.AddRazorPages()
.AddMicrosoftIdentityUI();
//Needed by the SyncAzureAdUsers code
services.Configure<AzureAdOptions>(_configuration.GetSection("AzureAd"));
services.RegisterAuthPermissions<Example5Permissions>(options =>
{
options.AppConnectionString = connectionString;
options.PathToFolderToLock = _env.WebRootPath;
})
.AzureAdAuthentication(AzureAdSettings.AzureAdDefaultSettings(false))
.UsingEfCoreSqlServer(connectionString)
.AddRolesPermissionsIfEmpty(Example5AppAuthSetupData.RolesDefinition)
.AddAuthUsersIfEmpty(Example5AppAuthSetupData.UsersRolesDefinition)
.RegisterAuthenticationProviderReader<SyncAzureAdUsers>()
.SetupAspNetCoreAndDatabase();
}
NOTE: It you are SURE that you won't have multiple instances, then you can set the options UseLocksToUpdateGlobalResources
property to false. This tells the Net.RunMethodsSequentially library that it can run the startup services without obtaining a global lock. See Example2's Startup class for an example of setting the UseLocksToUpdateGlobalResources
property to false.
In cases where you want to migrate and/or seed your own database on startup, then you can add extra startup services to the Net.RunMethodsSequentially inside AuthP. The Net.RunMethodsSequentially will each of your startup services (and the AuthP startup services) within global lock. This means if you have multiple instances of your app the startup services in each instance can't run at the same time as other startup services in another instance. But remember - each instance WILL run the startup services, so make sure your startup services check if the database has already been updated.
- If you want to run EF Core's
Migrate
method on startup, then there is theStartupServiceMigrateAnyDbContext<TContext>
class that can do that for you. - If you want to run method on startup, say to seed a database, then you need to create a class that inherits the
IStartupServiceToRunSequentially
interface.
The code below is taken from Example3's Startup class and shows the SetupAspNetCoreAndDatabase
method where you can add four extra startup services that will be run on startup.
services.RegisterAuthPermissions<Example3Permissions>(options =>
{
options.TenantType = TenantTypes.SingleLevel;
options.AppConnectionString = connectionString;
options.PathToFolderToLock = _env.WebRootPath;
})
//NOTE: This uses the same database as the individual accounts DB
.UsingEfCoreSqlServer(connectionString)
.IndividualAccountsAuthentication()
.RegisterTenantChangeService<InvoiceTenantChangeService>()
.AddRolesPermissionsIfEmpty(Example3AppAuthSetupData.RolesDefinition)
.AddTenantsIfEmpty(Example3AppAuthSetupData.TenantDefinition)
.AddAuthUsersIfEmpty(Example3AppAuthSetupData.UsersRolesDefinition)
.RegisterFindUserInfoService<IndividualAccountUserLookup>()
.RegisterAuthenticationProviderReader<SyncIndividualAccountUsers>()
.AddSuperUserToIndividualAccounts()
.SetupAspNetCoreAndDatabase(options =>
{
//Migrate individual account database
options.RegisterServiceToRunInJob<StartupServiceMigrateAnyDbContext<ApplicationDbContext>>();
//Add demo users to the database (if no individual account exist)
options.RegisterServiceToRunInJob<StartupServicesIndividualAccountsAddDemoUsers>();
//Migrate the application part of the database
options.RegisterServiceToRunInJob<StartupServiceMigrateAnyDbContext<InvoicesDbContext>>();
//This seeds the invoice database (if empty)
options.RegisterServiceToRunInJob<StartupServiceSeedInvoiceDbContext>();
});
I don't expect that many people use the Bulk Load feature, but I use it in the examples in the github.com/JonPSmith/AuthPermissions.AspNetCore repo so that you can run the examples with demo data.
Version 2 has a number of new features around Roles and Tenants which required extra data. So if you do use the Bulk Load feature, then you have to change:
- For Roles you need to use a list of
BulkLoadRolesDto
classes instead of a string. - For Tenants you need to use a list of
BulkLoadTenantDto
classes instead of a string.
The code shown below is the Version 2 setup of Roles and Tenants in Example3 that matches the Version 1 setup of Roles and Tenants in Example3.
public static readonly List<BulkLoadRolesDto> RolesDefinition = new List<BulkLoadRolesDto>()
{
new("SuperAdmin", "Super admin - only use for setup", "AccessAll"),
new("App Admin", "Overall app Admin",
"UserRead, UserSync, UserChange, UserRolesChange, UserChangeTenant, " +
"UserRemove, RoleRead, RoleChange, PermissionRead, IncludeFilteredPermissions, " +
"TenantList, TenantCreate, TenantUpdate"),
new("Tenant Admin", "Tenant-level admin", "InvoiceRead, EmployeeRead, EmployeeRevokeActivate"),
new("Tenant User", "Can access invoices", "InvoiceRead, InvoiceCreate"),
};
public static readonly List<BulkLoadTenantDto> TenantDefinition = new List<BulkLoadTenantDto>()
{
new("4U Inc."),
new("Pets Ltd."),
new("Big Rocks Inc."),
};
While adding new multi-tenant features to this library I found a bug in the use of the DataKey
which can cause hierarchical multi-tenant applications to sometimes access another tenant’s data. This is therefore a critical bug.
Version 2 of the library fixes to the bug, but does need a migrate any application's databases that use the DataKey
. Single multi-tenant applications in version 1 didn’t have a bug, but the AuthP DataKey
uses the same DataKey
for Single and Hierarchical multi-tenant, so you still need to follow this information when upgrading to Version 2.
So, if you are upgrading a version 1 Single and Hierarchical multi-tenant application that uses the DataKey
to version 2 you need to follow these steps:
For performance reasons I have changed the SQL type of the DataKey
from nvarchar
to varchar
, and set a size. If you are using AuthP’s DataKeyQueryExtension
extension methods, then these changes are automatically applied, and therefore will force you to create a EF Core migration.
So run the Add-Migration
command in VS2022's Package Manager Console to create a new migration
Once you have created a new migrate your application’s database you have to manually add some code to the new migration before you run it. I have created a Version2DataKeyHelper class which contains the method called UpdateToVersion2DataKeyFormat
that you can add for each entity that has a DataKey
. Because of the changes in step 1 the migration will contain a change to every table that has a DataKey
columns. The code below is taken from Example4 “Version2” migration.
public partial class Version2 : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<string>(
name: "DataKey",
schema: "retail",
table: "ShopStocks",
type: "varchar(250)",
unicode: false,
maxLength: 250,
nullable: true,
oldClrType: typeof(string),
oldType: "nvarchar(450)",
oldNullable: true);
//MANUALLY ADD THIS CODE FOR EVERY TABLE CONTAINING A DATAKEY
migrationBuilder.UpdateToVersion2DataKeyFormat("retail.ShopStocks");
migrationBuilder.AlterColumn<string>(
name: "DataKey", //.....
//Rest of code left out
}
}
As you can see in the code above, I manually added the call to the UpdateToVersion2DataKeyFormat
extension method, with the name of the table at a parameter, for every table that has a DataKey
.
NOTE: The UpdateToVersion2DataKeyFormat
method changes the DataKey
string to match the Version 2 format. If you want to go back to Version 1 you will need to create a similar method that returns the DataKey
string to the Version 1 format. If you can’t work out how to do that, then open an issue and I can create one.
In Version 2 the Roles that a Tenant Admin can see has changed, so in multi-tenant applications you now need to provide the Id of the logged-in user. This is pretty easy as shown in the code taken for Example3's RoleController.
[HasPermission(Example4Permissions.RoleRead)]
public async Task<IActionResult> Index(string message)
{
var userId = User.Claims.GetUserIdFromClaims();
var permissionDisplay = await
_authRolesAdmin.QueryRoleToPermissions(userId).ToListAsync();
ViewBag.Message = message;
return View(permissionDisplay);
}
END