Skip to content

SetupAspNetCoreAndDatabase

Jon P Smith edited this page Aug 22, 2022 · 5 revisions

The SetupAspNetCoreAndDatabase extension method used to register / configure the AuthP library is designed to migrate / seed databases on startup of the ASP.NET Core. In AuthP version 1 there was a limitation that this wouldn't work if your application has multiple instances of the applciation were running at the same time (known as Scale Out in Azure and Horizontally Scaling in Amazon).

Version 2 of the AuthP library fixed this limitation by using the Net.RunMethodsSequentially library uses a lock on a global resource (i.e. an resource all the app instances can access) which means your startup code are executed serially, and never in parallel.

Registering AuthP library, including SetupAspNetCoreAndDatabase

The SetupAspNetCoreAndDatabase needs a global resource to create a lock on, and a database is a good source. But there are some cases that the database doesn't exist, so we need a second global resource to use if the database isn't there. chosen as a FileSystem Directory as the backup, and in ASP.NET Core I use wwwRoot directory found by IWebHostEnvironment.WebRootPath.

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, but the bulk load parts were removed as most people won't use them.

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.PathToFolderToLock = _env.WebRootPath;
        })
        .AzureAdAuthentication(AzureAdSettings.AzureAdDefaultSettings(false))
        .UsingEfCoreSqlServer(connectionString)
        .RegisterAuthenticationProviderReader<SyncAzureAdUsers>()
        .SetupAspNetCoreAndDatabase();
}

In this case the SetupAspNetCoreAndDatabase will execute an migration of the AuthP's database on the startup of the application.

NOTE: This example comes from the older Startup class approach. If you want to see an example of how you would get the connection string and the WebRootPath in a Net6' 'Program' class approach, then have a look at the 'Program' class with the connection / WebRootPath lines highlighted.

Telling AuthP you aren't running multiple instances

It you are SURE that you won't have multiple instances, then you can set the AuthP' option 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.

OPTIONAL: Building/Running your own migrate / seeding code on startup

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 the SetupAspNetCoreAndDatabase method. The Net.RunMethodsSequentially will run 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.

Creating startup services to run on startup of

If you want to create a startup service you need to create a class that inherits the IStartupServiceToRunSequentially interface. I recommend you read this section of the article about the Net.RunMethodsSequentially library which covers this in detail. You can also find a number of implemented startup services in the AuthP's StartupSevices directory that might help you.

If you want to run EF Core's MigrateAsync method on startup on your DbContext, then there is a generic startup service called StartupServiceMigrateAnyDbContext<TContext> where you can provide your DbContext class.

WARNING: If you have multiple EF Core multiple DbContexts, then read this

When using the AuthP library you often have multiple DbContexts, e.g. in Example3 it has 1) the individual users account DbContext, 2) the AuthP DbContext, and 3) the tenant DbContext. In this case you MUST ensure each DbContext has a unique named migration table. You can set the name of the DbContext's migration table via the MigrationsHistoryTable extension method.

The AuthP's registering code includes a call to the MigrationsHistoryTable when registering the AuthP's Dbcontext, but you need to add a MigrationsHistoryTable when registering your tenant data DbContext - see the code below taken from Example3 that sets up the tenant DbContext.

services.AddDbContext<InvoicesDbContext>(options =>
    options.UseSqlServer(
        configuration.GetConnectionString("DefaultConnection"), dbOptions =>
    dbOptions.MigrationsHistoryTable(InvoicesDbContextHistoryName)));

Registering your startup services

To register your extra startup services you need to use the RegisterServiceToRunInJob<TService> inside the SetupAspNetCoreAndDatabase` options.

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>();
    });

NOTE: This runs 6 startup services: the four you can see being added manually via the SetupAspNetCoreAndDatabase options, plus the migration of the AuthP DbContext and the running of the bulk load setup service that seeds the database with the example Roles, Tenants and AuthP users.

Getting your startup services run in the correct order

Some startup services have to be run in a certain order, for instance if you wish to seed a database, then should first migrate the database. The order in which startup services is defined by the startup service's OrderNum property. The OrderNum defines define the order you want your startup services are run, but if startup services has the same OrderNum value, then the startup services will run in the order they were registered.

Within the AuthP the built-in services the OrderNum is as follows

  • StartupServiceMigrateAnyDbContext<TContext> has an OrderNum of -10, so should run first. NOTE: there may be multiple services using the startup service, but they shouldn't interfere with each other.
  • StartupServiceMigrateAuthPDatabase has an OrderNum of -5. This runs after any other migration, but before any bulk load startup services.
  • StartupServiceIndividualAccountsAddSuperUser<TIdentityUser> has an OrderNum of -1. This must be after migrations, and after the adding demo users startup service
  • StartupServiceBulkLoadAuthPInfo has an OrderNum of 0, so it runs after the StartupServiceMigrateAuthPDatabase startup service.

NOTE: I have left out some of the startup services that are used to set up demo data, such as StartupServicesIndividualAccountsAddDemoUsers from this list.

So, if you are using the generic StartupServiceMigrateAnyDbContext<TContext> to migrate your database (which has an OrderNum of -10), then your seed database startup service can have a OrderNum of 0 (default value) and will be run after the migration.

Articles / Videos

Concepts

Setup

Usage

Admin

SupportCode

Clone this wiki locally