You can follow this documentation: https://abp.io/docs/latest/framework/infrastructure/event-bus/local#transaction-exception-behavior
It refers;
Event handlers are always executed in the same unit of work scope, that means in the same database transaction with the code that published the event. If an event handler throws an exception,
It doesn't support multiple databases or connection strings. Runs all your operations in the same unit of work. That means there is no single database transaction that depends on each other. When you face a problem they'll rollback separately according to your scenario. You may visit: https://abp.io/docs/latest/framework/architecture/domain-driven-design/unit-of-work#unit-of-work
If you use different repositories and DbContexts within the same Unit of Work (UOW), it will still roll back both while using LocalEventBus or directly coding in the same method with 2 different repositories or DbContext instances. This is a feature of Unit of Work and not a native database transaction
so you say even if it is a different database, unitofwork is going to rollback the transaction?
Unit Of Work is database agnostic, it doesn't know which database are you using. If one of your repositories throws an exception while committing it'll try rollback all.
Here's a sequence of what happens:
1. DbContext1 (Database1) - Succeeds
2. DbContext2 (Database2) - Throws Exception
3. UnitOfWork catches exception
4. UnitOfWork calls RollbackAllAsync()
5. Both DbContext1 and DbContext2 get rollback attempts
This behavior ensures atomicity across multiple databases - either all operations succeed, or none of them do. This is particularly important for distributed transactions where you need to maintain consistency across different data stores.
Hi
There no such feature neither in ABP Framework and Entity Framework Core.
Normally, transaction between different databases is not possible but some specific cases. As far as I know, it can be possible in SqlServer somehow but making it stable for all scenarios is another tough topic. You can go with this implementation but you have to implement it by yourself. https://learn.microsoft.com/en-us/dotnet/framework/data/adonet/distributed-transactions
We suggest using EventBus for this kind of scenarios. You can publish an event to update the other database and go on.
If you use LocalEventBus, it will be a local transaction, and whenever an exception occurs in the consumer side, the transaction from the publisher side will be rolled back.
If you use DistributedEventBus, it will be a distributed transaction, and whenever an exception occurs in the consumer side, it'll be retried until it becomes successful. You should handle this in your consumer side and make it eventually consistent.
Note:
Make sure you're using Outbox / Inbox for Transactional Events feature for the distributed event bus.
Hi, we fixed this problem and it'll be published as a patch version of LeptonX v4.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:
... 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");
});
}
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:
https://abp.io/docs/latest/framework/fundamentals/connection-strings#replace-the-connection-string-resolver
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-L79
But 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.
MicroServiceConnectionStringResolver.cs and disable fallbacking to Tenant's default connectionstringusing 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
Ok,
I could reproduce the problem. I'll check and find problem & solution soon
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?
Seems like outdated issue, create new one if the problem still exists. (Your credit has been refunded)
Hi,
Localizations comes from here: https://github.com/abpframework/abp/blob/664af59cd7778b40b635540d6213d7b6b0985dea/framework/src/Volo.Abp.AspNetCore.Mvc.UI.MultiTenancy/Volo/Abp/AspNetCore/Mvc/UI/MultiTenancy/Localization/en.json#L4-L11
You can add this keys into localization json file and override AbpUiMultiTenancyResource by following this documentation:
https://abp.io/docs/latest/framework/fundamentals/localization#extending-existing-resource
Configure<AbpLocalizationOptions>(options =>
{
// ...
options.Resources
.Get<AbpUiMultiTenancyResource>()
.AddVirtualJson("/Localization/YourProjectResource");
// ...
});
Note that: Your
.Domain.Sharedmodule probably won't have reference forAbpUiMultiTenancyResourceclass. So, you'll need to do that in the host application such as.WeborHttpApi.Hostor.AuthServeraccording to your configuration.