Check the docs before asking a question: https://abp.io/docs/latest
Check the samples to see the basic tasks: https://abp.io/docs/latest/samples
The exact solution to your question may have been answered before, and please first use the search on the homepage.
Provide us with the following info:
🧐 Hint: If you are using the ABP Studio, you can see all the information about your solution from the configuration window, which opens when you right-click on the solution and click on the Solution Configuration
button.
-
Template: microservice
-
Created ABP Studio Version: 0.7.7
-
UI Framework: blazor
-
Theme: leptonx
-
Theme Style: system
-
Database Provider: ef
-
Database Management System: sqlserver
-
Mobile Framework: none
-
Public Website: No
-
ABP Framework version: v8.2.0
-
UI Type: Blazor WASM
-
Database System: EF Core (SQL Server)
-
Tiered (for MVC) or Auth Server Separated (for Angular): -
-
Exception message and full stack trace:
-
Steps to reproduce the issue:
Hello team, how are you?
I have a solution created from the Microservices template that is Multitenant.
I want to configure a new tenant with a dedicated database, whether it is a single database for all microservices or one database per microservice. I am encountering the following problems:
1. If I try to configure a single database for the tenant, I get the error "There is already an object named 'AbpEventInbox' in the database." This is logical because each microservice implements these tables.
What would be the way to configure a single database for all microservices?
2. If I try to configure a database for each microservice, the SaaS module does not show me all the microservices, it only shows "Administration". Looking at the source code of the SaaS module, I saw that it reads from TenantAppService.GetDatabasesAsync, I understand that it takes them from "AbpDbConnectionOptions".
<br>
Then I look at the configuration of each microservice, and they are like this:
SaasService:
IdentityService:
AdministrationService:
This makes me think that the SaaS module is only reading what is configured in the "SaasService" microservice.
How should this work, or what would be the correct way to define this so that it functions as expected?
10 Answer(s)
-
0
Hi,
I'm trying to reproduce this problem but before I go, I need some information
Did you add any entity framework core migration manually in this scenario?
-
0
Ok,
I could reproduce the problem. I'll check and find problem & solution soon
-
1
I found the root of the problem.
It happens because of the fallback to the default connection string.
👇
https://github.com/abpframework/abp/blob/74d516829be7f05cfae7d4a67f18591b41e5446a/framework/src/Volo.Abp.MultiTenancy/Volo/Abp/MultiTenancy/MultiTenantConnectionStringResolver.cs#L76-L79But in the micro-service solution, each service uses their own named connection string, not default one. But
MultiTenantConnectionStringResolver
fallbacks to the default connection string and it happens on each service.Here the workaround that I found and make it work for now.
-
Create a new
MicroServiceConnectionStringResolver.cs
and disable fallbacking to Tenant's default connectionstring
using System; using Microsoft.Extensions.Options; using Volo.Abp.Data; using Volo.Abp.DependencyInjection; using Volo.Abp.MultiTenancy; namespace AbpSolution1.AuthServer; [Dependency(ReplaceServices = true)] [ExposeServices(typeof(IConnectionStringResolver), typeof(DefaultConnectionStringResolver))] public class MicroServiceConnectionStringResolver : MultiTenantConnectionStringResolver { private readonly ICurrentTenant _currentTenant; private readonly IServiceProvider _serviceProvider; public MicroServiceConnectionStringResolver(IOptionsMonitor<AbpDbConnectionOptions> options, ICurrentTenant currentTenant, IServiceProvider serviceProvider) : base(options, currentTenant, serviceProvider) { _currentTenant = currentTenant; _serviceProvider = serviceProvider; } public override async Task<string> ResolveAsync(string? connectionStringName = null) {if (_currentTenant.Id == null) { //No current tenant, fallback to default logic return await base.ResolveAsync(connectionStringName); } var tenant = await FindTenantConfigurationAsync(_currentTenant.Id.Value); if (tenant == null || tenant.ConnectionStrings.IsNullOrEmpty()) { //Tenant has not defined any connection string, fallback to default logic return await base.ResolveAsync(connectionStringName); } var tenantDefaultConnectionString = tenant.ConnectionStrings?.Default; //Requesting default connection string... if (connectionStringName == null || connectionStringName == ConnectionStrings.DefaultConnectionStringName) { //Return tenant's default or global default return !tenantDefaultConnectionString.IsNullOrWhiteSpace() ? tenantDefaultConnectionString! : Options.ConnectionStrings.Default!; } //Requesting specific connection string... var connString = tenant.ConnectionStrings?.GetOrDefault(connectionStringName); if (!connString.IsNullOrWhiteSpace()) { //Found for the tenant return connString!; } //Fallback to the mapped database for the specific connection string var database = Options.Databases.GetMappedDatabaseOrNull(connectionStringName); if (database != null && database.IsUsedByTenants) { connString = tenant.ConnectionStrings?.GetOrDefault(database.DatabaseName); if (!connString.IsNullOrWhiteSpace()) { //Found for the tenant return connString!; } } // Disable fallback to tenant's default connection string as A WORKAROUBD ////Fallback to tenant's default connection string if available // if (!tenantDefaultConnectionString.IsNullOrWhiteSpace()) // { // return tenantDefaultConnectionString!; // } return await base.ResolveAsync(connectionStringName); } }
You can create a new module and share this class across all the services to replace
IConnectionStringResolver
implementation in all the services.
In my case, I went without inbox-outbox pattern, but still face this problem when changed tenant's connectionstring:
I only replaced this file for
AuthServer
and it worked. But you'll need to override this service for each service because of inbox/outbox tables -
-
0
I found the root of the problem.
It happens because of the fallback to the default connection string.
👇
https://github.com/abpframework/abp/blob/74d516829be7f05cfae7d4a67f18591b41e5446a/framework/src/Volo.Abp.MultiTenancy/Volo/Abp/MultiTenancy/MultiTenantConnectionStringResolver.cs#L76-L79But in the micro-service solution, each service uses their own named connection string, not default one. But
MultiTenantConnectionStringResolver
fallbacks to the default connection string and it happens on each service.Here the workaround that I found and make it work for now.
-
Create a new
MicroServiceConnectionStringResolver.cs
and disable fallbacking to Tenant's default connectionstring
using System; using Microsoft.Extensions.Options; using Volo.Abp.Data; using Volo.Abp.DependencyInjection; using Volo.Abp.MultiTenancy; namespace AbpSolution1.AuthServer; [Dependency(ReplaceServices = true)] [ExposeServices(typeof(IConnectionStringResolver), typeof(DefaultConnectionStringResolver))] public class MicroServiceConnectionStringResolver : MultiTenantConnectionStringResolver { private readonly ICurrentTenant _currentTenant; private readonly IServiceProvider _serviceProvider; public MicroServiceConnectionStringResolver(IOptionsMonitor<AbpDbConnectionOptions> options, ICurrentTenant currentTenant, IServiceProvider serviceProvider) : base(options, currentTenant, serviceProvider) { _currentTenant = currentTenant; _serviceProvider = serviceProvider; } public override async Task<string> ResolveAsync(string? connectionStringName = null) {if (_currentTenant.Id == null) { //No current tenant, fallback to default logic return await base.ResolveAsync(connectionStringName); } var tenant = await FindTenantConfigurationAsync(_currentTenant.Id.Value); if (tenant == null || tenant.ConnectionStrings.IsNullOrEmpty()) { //Tenant has not defined any connection string, fallback to default logic return await base.ResolveAsync(connectionStringName); } var tenantDefaultConnectionString = tenant.ConnectionStrings?.Default; //Requesting default connection string... if (connectionStringName == null || connectionStringName == ConnectionStrings.DefaultConnectionStringName) { //Return tenant's default or global default return !tenantDefaultConnectionString.IsNullOrWhiteSpace() ? tenantDefaultConnectionString! : Options.ConnectionStrings.Default!; } //Requesting specific connection string... var connString = tenant.ConnectionStrings?.GetOrDefault(connectionStringName); if (!connString.IsNullOrWhiteSpace()) { //Found for the tenant return connString!; } //Fallback to the mapped database for the specific connection string var database = Options.Databases.GetMappedDatabaseOrNull(connectionStringName); if (database != null && database.IsUsedByTenants) { connString = tenant.ConnectionStrings?.GetOrDefault(database.DatabaseName); if (!connString.IsNullOrWhiteSpace()) { //Found for the tenant return connString!; } } // Disable fallback to tenant's default connection string as A WORKAROUBD ////Fallback to tenant's default connection string if available // if (!tenantDefaultConnectionString.IsNullOrWhiteSpace()) // { // return tenantDefaultConnectionString!; // } return await base.ResolveAsync(connectionStringName); } }
You can create a new module and share this class across all the services to replace
IConnectionStringResolver
implementation in all the services.
In my case, I went without inbox-outbox pattern, but still face this problem when changed tenant's connectionstring:
I only replaced this file for
AuthServer
and it worked. But you'll need to override this service for each service because of inbox/outbox tablesGood morning,
In response to the question you asked in your first comment, I am not sure I understand the question. Nevertheless, I will mention that in the microservices that the template provides by default (administration, identity, saas), only the standard initial migration was created.
Now, regarding the solution you mentioned, it seems to me that it does not correspond to my problem.
If I create a tenant and check 'Use shared database,' the tenant is created correctly and the connection string is resolved properly.
The two points or issues I report are:
Point 1:
If I want to define a new tenant that uses a unique database (as I showed in the first image), I understand that the connection string is resolved correctly, but because of how the template is designed, each microservice tries to create the "AbpEventInvox"/"AbpEventOutbox" tables, which is one of the points I reported. I understand that this is more of a design issue than a connection string resolution problem. I believe that the names of the tables AbpEventInvox/AbpEventOutbox should include part of the microservice name, so that if it is configured to use a single database, the names do not clash. Is that possible? I think that could be a potential solution to this point.
Point 2:
If I want to define a new tenant that uses its own database for each microservice, I believe there is an error in the saas module; it does not allow you to select each of the existing services to define their corresponding connection string.
I would like to be able to configure these two scenarios, and it seems to me that the solution you provided does not resolve this but only addresses how the connection string is resolved. If I am mistaken, please let me know.
Thank you very much in advance!
I look forward to your comments."
-
-
0
I got it better right now. Micro-Service template is renewed in a near history. It seems it's a design decision. Still I deliver your feedbacks to the team and we'll consider these for some changes.
Here some suggestions for your points for now:
Point 1
... AbpEventInvox/AbpEventOutbox should include part of the microservice name
You can it happen by configuring in your each Services's DbContext:
protected override void OnModelCreating(ModelBuilder builder) { // ... var microserviceName = "IdentityService"; builder.Entity<IncomingEventRecord>(b => { b.ToTable($"{microserviceName}_AbpEventInbox"); }); builder.Entity<OutgoingEventRecord>(b => { b.ToTable($"{microserviceName}_AbpEventOutbox"); }); }
Point 2
By referring to Connection String - configuring-the-database-structures documentation, you can create groups with multip connection string names like this:
Configure<AbpDbConnectionOptions>(options => { options.Databases.Configure("MyDatabaseGroup", db => { db.MappedConnections.Add("Administration"); db.MappedConnections.Add("AuditLoggingService"); db.MappedConnections.Add("AbpBlobStoring"); db.MappedConnections.Add("SaasService"); // ... }); });
And then, you can configure it for using in the SAAS module, since saas does not show all the connecting string by default. You have to configure them to make them selectable for each tenant in saas module:
Configure<AbpDbConnectionOptions>(options => { options.Databases.Configure("MyDatabaseGroup", database => { database.IsUsedByTenants = true; }); });
But you want use separate databases for each service. So you make make tem one by one available to choose in Saas Module:
Configure<AbpDbConnectionOptions>(options => { options.Databases.Configure("Administration", database => { database.IsUsedByTenants = true; }); options.Databases.Configure("Identity", database => { database.IsUsedByTenants = true; }); options.Databases.Configure("AuditLogging", database => { database.IsUsedByTenants = true; }); // ...});
Or if you make it automatically instead choosing always in the UI, you can eeplace the Connection String Resolver completely and just append tenant name and requested connectionstring name to the database name in the connectionstring by following this:
-
0
I got it better right now. Micro-Service template is renewed in a near history. It seems it's a design decision. Still I deliver your feedbacks to the team and we'll consider these for some changes.
Here some suggestions for your points for now:
#### Point 1
... AbpEventInvox/AbpEventOutbox should include part of the microservice name
You can it happen by configuring in your each Services's DbContext:
protected override void OnModelCreating(ModelBuilder builder) { // ... var microserviceName = "IdentityService"; builder.Entity<IncomingEventRecord>(b => { b.ToTable($"{microserviceName}_AbpEventInbox"); }); builder.Entity<OutgoingEventRecord>(b => { b.ToTable($"{microserviceName}_AbpEventOutbox"); }); }
#### Point 2
By referring to Connection String - configuring-the-database-structures documentation, you can create groups with multip connection string names like this:Configure<AbpDbConnectionOptions>(options => { options.Databases.Configure("MyDatabaseGroup", db => { db.MappedConnections.Add("Administration"); db.MappedConnections.Add("AuditLoggingService"); db.MappedConnections.Add("AbpBlobStoring"); db.MappedConnections.Add("SaasService"); // ... }); });
And then, you can configure it for using in the SAAS module, since saas does not show all the connecting string by default. You have to configure them to make them selectable for each tenant in saas module:
Configure<AbpDbConnectionOptions>(options => { options.Databases.Configure("MyDatabaseGroup", database => { database.IsUsedByTenants = true; }); });
But you want use separate databases for each service. So you make make tem one by one available to choose in Saas Module:
Configure<AbpDbConnectionOptions>(options => { options.Databases.Configure("Administration", database => { database.IsUsedByTenants = true; }); options.Databases.Configure("Identity", database => { database.IsUsedByTenants = true; }); options.Databases.Configure("AuditLogging", database => { database.IsUsedByTenants = true; }); // ...});
Or if you make it automatically instead choosing always in the UI, you can eeplace the Connection String Resolver completely and just append tenant name and requested connectionstring name to the database name in the connectionstring by following this:
Hello Enisn, sorry for the delay, I was tied up with another issue.
Point 1: What you mentioned solves the problem! The unique database for all microservices is created correctly. The only minor detail I see at the moment is that the tables for the SaaS module are being created, even though these tables should always be used from the host and should not be in the tenant database.
Point 2: I understand the explanation, but here's what happens. I have added a new microservice, "GeneralService", which is multi-tenant and works correctly.
If I configure the module for "GeneralService" as you explained:
The SaaS module UI still does not show this microservice to configure the connection string per service.
Upon testing, I see that the SaaS module UI only includes what is defined in the microservice SaaSService module. For example, if I add the following:
This is added:
What would be the correct way for my microservice "GeneralService" to be selected in the SaaS UI?
I look forward to your comments; thank you very much!
-
0
Upon testing, I see that the SaaS module UI only includes what is defined in the microservice SaaSService module.
Yes this is right. There is no synchronization between services for this configuration. This is a simple Options Pattern from the built-in .NET Dependency Injection. Configuring it Saas make sense, but still you'll need to apply this configuration where the connection string is resolved. You may create a simple class library or module (easy to create with ABP Studio with right clicking the soluion) and configure it in there an share this configuration across all the services. Some of them maybe not required for some services but managing it one by one is harder than sharing all the configuration from single point
-
0
Upon testing, I see that the SaaS module UI only includes what is defined in the microservice SaaSService module.
Yes this is right. There is no synchronization between services for this configuration. This is a simple Options Pattern from the built-in .NET Dependency Injection. Configuring it Saas make sense, but still you'll need to apply this configuration where the connection string is resolved. You may create a simple class library or module (easy to create with ABP Studio with right clicking the soluion) and configure it in there an share this configuration across all the services. Some of them maybe not required for some services but managing it one by one is harder than sharing all the configuration from single point
Could it be that the 'LanguageManagement' module has any particular behavior regarding the resolution of connection strings?
I create a new tenant 'E01', define the default connection string and a connection string for each service, then upon logging in, I select the tenant and when I click 'Save' it gives the following error:
I understand that it's a connection string resolution problem, but now I believe I have everything well defined.
The connection string for LanguageManager is defined in the 'Administration' Database along with others.
That definition is included in the 'Module' file of each application that has data access (microservices and auth server).
Do you see any reason why it might be resolving a different connection string than the one defined in 'Administration'?
On the other hand, if I remove the 'Administration' connection string from tenant 'E01' and test it again, the tables are now created in the default database and the login works as expected. This suggests to me that when resolving, it defaults to the default connection string even if there is a specific one.
loggin ok!
-
0
The following is the log from 'authserver'. I used a custom resolver that is a copy of the original 'MultiTenantConnectionStringResolver' and added functionality to log information; I don't know if this might help in finding the problem. I do not see any moment where the connection string for 'AbpLanguageManager' for 'Administration' is requested.
This confuses me more; I expected to see something different. Does this mean anything to you?
-
0
Hello,
Sorry for the late reply, but we missed this question because the friend dealing with the subject was on vacation. However, next Monday, the friend who is interested in the subject will return to you. Thank you for your patience.