I was thinking that If I added a row to the abpUsers table for each tenant that the user is linked to but use the abpUserAccounts table to do the actaual login... So Username/Email would be unique in abpUserAccounts but will exist multiple times in abpUsers if the user is linked to multiple teants? Then when the abpUserAccount has been logged in, they will then choose an abpUser to use for the session and provide a way of switching this in the user menu, like you do for Linked Accounts? We work with Groups of schools and certain users will need access to all the schools in their groiup.
Thank you @jeffmh and @ismcagdas for all your help, I love asp net zero and getting support through the forum is great!
So my current thinking is that maybe I move the authentication fields from abpUsers to abpUserAccounts. Log the user in with the info in abpUserAccounts. Each "UserAccount" will have a row in the abpUsers table for each tenant they are linked with. After logging in, they will select the Tenant they want from the ones they are linked with, which will effectivly log them in using the abpUser row? Does this make sense or is there a better approach?
Ok so I think I have pretty much got the multiple db context thing figured out now so thank you for all your help! It was mostly the connectionstringresolver I needed. Here is a simpified version of it, just incase it helps anyone else:
namespace MyProject.EntityFrameworkCore
{
public class MyConnectionStringResolver : DefaultConnectionStringResolver
{
private readonly IConfigurationRoot _appConfiguration;
private readonly ICurrentUnitOfWorkProvider _currentUnitOfWorkProvider;
private readonly ITenantCache _tenantCache;
public MyConnectionStringResolver(
IAbpStartupConfiguration configuration,
ICurrentUnitOfWorkProvider currentUnitOfWorkProvider,
ITenantCache tenantCache)
: base(configuration)
{
_currentUnitOfWorkProvider = currentUnitOfWorkProvider;
_tenantCache = tenantCache;
}
public override string GetNameOrConnectionString(ConnectionStringResolveArgs args)
{
if (args["DbContextConcreteType"] as Type == typeof(MyProjectDbContextSecond))
{
var tenantCacheItem = _tenantCache.Get(_currentUnitOfWorkProvider.Current.GetTenantId().Value);
return tenantCacheItem.ConnectionString;
}
return base.GetNameOrConnectionString(args);
}
}
}
So now all my main aspnetzero data is accessed from the Host Db and My custom tenant specific entities are stored in their own databases. Is this the best way to achieve it or is there a better way to identify which Connection string to use without a Type comparison? I have tried marking my second dbcontext with
[MultiTenancySide(MultiTenancySides.Tenant)]
But args.MultiTenancySide is always Host or Null? I was thinking maybe I could have some sort of dbcontext interface with a property on it I could check to see what dbcontext to use?
Anyway, my next issue is that I am being asked to allow users to have access to multiple tenants and be able to set roles against users and tenants. My users all sign up with a unique email address and I want them to only exist once. I cant use the linked account or "Delegation" as this requires a user to map to from the tenant linked to. I could add a table for this but I am not sure it will work as even if they can login, they wont get the right data because of the tenant filters? What is the best aproach here?
I have had another go and done some more googling but i still cant get it to work! If you would be interested in building a solution for me for a fee please do let me know how I can contact you directly! It would be a big help!
Thanks Jeffmh, I am still not sure I fully understand but I relly apreciate your help! I am not sure if this is allowed on here but if you were interested in some paid work to set this up for me I would be more than happy to pay! I am quite short on time so as much as I am determind to learn, it may be best for me to just get an expert in and then I can go over the code?
Thanks Jeffmh, I have duplicated the AbpZeroDbMigrator class and renamed it and put the correct db context on it. I also already created a Factory and Configurer for my new dbContext so I think I have all the right bits now, I am just not sure ow to modify the Tenant Manager to create the correct db context? I can see where the db is created:
//Create tenant database
_abpZeroDbMigrator.CreateOrMigrateForTenant(tenant);
Do I just need to Add my custom Migrator to the constructor and it will automatically get injected? Appologies for my naievaty, I am a winforms developer tasked with this huge project!
Thank you jeffmh, that is helpful! Sorry for the late reply, for some reason I didnt get a notification of your reply!
Ok so I have created a new aspnetzero solution and made the following changes:
Added a new custom db context and added a "Person" entity to it manually
namespace Cloud.EntityFrameworkCore
{
[MultiTenancySide(MultiTenancySides.Tenant)]
public class CloudDbContextSecond : AbpDbContext
{
/* Define an IDbSet for each entity of the application */
public virtual DbSet<People> People { get; set; }
public CloudDbContextSecond(DbContextOptions<CloudDbContextSecond> options) : base(options)
{
}
protected override void OnModelCreating(ModelBuilder modelBuilder) { }
}
}
Update the main db context to be Host:
namespace Cloud.EntityFrameworkCore
{
[MultiTenancySide(MultiTenancySides.Host)]
public class CloudDbContext : AbpZeroDbContext<Tenant, Role, User, CloudDbContext>, IAbpPersistedGrantDbContext
{
Added a custom connection string resolver
namespace Cloud.EntityFrameworkCore
{
public class MyConnectionStringResolver : DefaultConnectionStringResolver
{
private readonly IConfigurationRoot _appConfiguration;
public MyConnectionStringResolver(IAbpStartupConfiguration configuration, IHostingEnvironment hostingEnvironment)
: base(configuration)
{
_appConfiguration =
AppConfigurations.Get(hostingEnvironment.ContentRootPath, hostingEnvironment.EnvironmentName);
}
public override string GetNameOrConnectionString(ConnectionStringResolveArgs args)
{
if (args["DbContextConcreteType"] as Type == typeof(CloudDbContextSecond))
{
return "?";
}
return base.GetNameOrConnectionString(args);
}
}
}
Updated CloudEntityFrameworkCoreModule to add the second db context and custom connection string resolver
public override void PreInitialize()
{
if (!SkipDbContextRegistration)
{
Configuration.ReplaceService<IConnectionStringResolver, MyConnectionStringResolver>();
Configuration.Modules.AbpEfCore().AddDbContext<CloudDbContext>(options =>
{
if (options.ExistingConnection != null)
{
CloudDbContextConfigurer.Configure(options.DbContextOptions, options.ExistingConnection);
}
else
{
CloudDbContextConfigurer.Configure(options.DbContextOptions, options.ConnectionString);
}
});
Configuration.Modules.AbpEfCore().AddDbContext<CloudDbContextSecond>(options =>
{
if (options.ExistingConnection != null)
{
CloudDbContextSecondConfigurer.Configure(options.DbContextOptions, options.ExistingConnection);
}
else
{
CloudDbContextSecondConfigurer.Configure(options.DbContextOptions, options.ConnectionString);
}
});
}
When a new tenant is being created, I suply a Connection String and it creates a database with the Main DB Context but creates the users etc in the Host Database, so I am sort of half way there! How do I tell it to create the db with the Second db context for new tenants? I also need to test CRUD on the Second DB context but I cant get the Entity Generator to create entities on the second db context? I have tried changing the config file but when it runs, it just hangs? Also, I am not sure it is actually going to get a connection string to the second db context anyway. Sorry for all the questions, just struggling to get my head around all this!
Thanks ismcagdas, I think that is exactly what I need. So I have followed this guide https://stackoverflow.com/questions/49243891/how-to-use-multiples-databases-in-abp-core-zero which sets up the project like the MultipleDbContextEfCoreDemo. Instead of creating the MyConnectionStringResolver I need have created a DbPerTenantConnectionStringResolver as per your example. Or do I need both? I can see how MyConnectionStringResolver reads the second connection string from the appsettings.json file but I am not sure how to use DbPerTenantConnectionStringResolver? I am assuming that like the MyConnectionStringResolver, I set it up like this In the CoreModule PreInitialize void?
Configuration.ReplaceService<IDbPerTenantConnectionStringResolver, DbPerTenantConnectionStringResolver>();
When I create a new tenant, I only have the choice to tell it to use the host database or supply a connection string? I want it to use the host database for everything EXCEPT the stuff on the Second DbContext I have created. When creating a tenant, it should create a new database for that tenant using the TenantDbContext I have created and set the connectionstring property for that tenant but I am not sure how to achieve this?
Thanks, I had seen that example but it works with two static connection strings but our case is quite complex.
We effectively have THREE types of Tenant: Host, Company and Alliance. Host is pretty self explanitory, company is a “Tenant” in the traditional sense of the ABP framework whereas an Alliance is basically a “Group” of companies. An “Alliance” user can login and view summary data from all of the “Companies” that it is linked with.
All user data for ALL Companies and Alliances should be stored in the main AspNetZero database, allowing us to have a single login page and no need to select a tenant when logging in. Users are unique and identified by their email address. It should be possible for a User to be associated with multiple Tenants. They may belong to a single tenant, e.g. using the TenantId in the users table, but another tenant should be able to give that same user access to their data. If a user is associated with multiple tenants, they will be given a choice of tenants to view after login, with the ability to Switch tenant later.
This all means that I think we need at least 2 dbContext that can be used simultaniously:
AspNetZeroContext This SINGLE database will contain all the standard ABP tables and store users, roles & permissions etc for all tenants. It will also have some custom tables for “Lookups” (Common data that all tenants will need READ ONLY access to) and CRM functionality such as Support Tickets and Invoices etc.
CompanyContext Each Tenant of type “Company” will have its own database that will contain ONLY custom entities, created using ASP.NET Zero Power Tools in visuall studio.e.g. Staff Entity. The Staff entity may have an “EthnicityID” field, this ID will be taken from a “Lookup” table stored in the Main AspNetZero database.
AllianceContext? This type of tenant may not need to be an actual dbContext as it wont have its own database, it will just need to be able to pull data from multiple "Company" databases.
We have written the code to get the second dbContext but I am unsure of the best aproach when we do not have a SINGLE connection string for the second dbContext, it will be tenant specific? Am I missing something here?