Activities of "enisn"

Answer

Hi @nabass

It seems the CSS variable is loaded much more earlier than your custom logo variable. The only way to prevent this problem permanently is replacing this physical files in your application:

/images/logo/leptonx/icon-logo.svg
/images/logo/leptonx/icon.svg

Overriding BrandingProvider also works but still there is a timing problem. When the theme's CSS file is loaded to the browser it automatcally starts download the given file from variable. Even you override, it was started download already. So replacing the original file prevents it.

Another approach is customizing the SCSS files and build your css on your own if you have access to source code of leptonx. But replacing the source file is much more easier solution for now

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

If i want to implement Inbox / Outbox pattern to my internal modules (with local events) i believe that is not possible with abp out of the box. Am i right about it?

Yes, you are correct LocalEventBus do not support the Inbox/Outbox pattern for local events by default. This pattern is indeed designed for distributed event scenarios.

If you want to implement the Inbox/Outbox pattern for internal modules using LocalEventBus, you will need to create a custom implementation. You may inherit and override LocalEventBus according your needs or you may create a new provider by implementing DistributedEventBusBase in a new class.

By the way, if you don't configure any distributed event bus provider, IDistributedEventBus works in memory like local event bus. So you can use this interface without configuring any provider such as rabbitmq or kafka.

But still it does not use outbox pattren by default, you see below it doesn't do anything with useOutbox parameter https://github.com/abpframework/abp/blob/69fde715c730b5796d32f7622ac7dc7782b6d320/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/LocalDistributedEventBus.cs#L138-L148

You can manually override one of them if you really need to use outbox/inbox with local events.

But as a recommendation;

  • I can suggest using RabbitMQ with ABP's default inbox/outbox implementation -- or --
  • I can suggest going with LocalEventBus and investigate performance, if a bottleneck detected, than you can take an action to switch other solutions

Just one more question. Does it have any performance problem since it is dealing with multiple db contexts with separate connection strings in one request? Cause i have seen in so many examples, they do not deal with unitofwork in multiple databases instead they have implemented inbox/outbox pattern and closing the transactions for each modules. So you can think they use separate unit of work for each module and use inbox/outbox pattern.

Short answer is yes.

This unit of work and rollback logic requires execution of all the db context operations in the same transaction and to end the request all the db context operations should be completed. HTTP Request lasts until all the operations are completed. It brings some drawbacks like performance issues. Also, it depends on the database operations and your deployment environment. But still it costs some performance instead of using single db context.

Even if you use LocalEventBus, still all the operations will be executed in the same transaction. So it won't make any difference.

But using Inbox/Outbox pattern will make some performance difference. Cause it will not require execution of all the operations in the same transaction. It will just send the messages to the outbox table and that's it. So it will not wait for the operation to complete.

But this case brings some other drawbacks like complexity and handling errors and retry mechanisms. ABP implements retry logic with Inbox/Outbox pattern, but still, you'll need to maintain and check the fails regularly. Some problems can't be eventually consistent. (Like you said, when database is not reachable, retry logic doesn't help until it is reachable again.)

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.

  1. When an exception occurs during the unit of work:
    • The RollbackAsync() method is called
    • It attempts to rollback ALL registered database APIs and transaction APIs
    • Each rollback operation is wrapped in a try-catch to ensure all DBs get rollback attempts
  2. For multiple DbContexts:
    • Each DbContext is registered as a separate database API
    • They can be connected to different databases
    • When one fails, the UnitOfWork will attempt to rollback all of them So to answer your question: Yes, both DbContexts will be rolled back even if they're connected to different databases. This is by design to maintain data consistency across your entire business transaction.

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:

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:

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.

  1. 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

Ok,

I could reproduce the problem. I'll check and find problem & solution soon

Showing 261 to 270 of 784 entries
Boost Your Development
ABP Live Training
Packages
See Trainings
Mastering ABP Framework Book
The Official Guide
Mastering
ABP Framework
Learn More
Mastering ABP Framework Book
Made with ❤️ on ABP v10.1.0-preview. Updated on December 17, 2025, 07:08
1
ABP Assistant
🔐 You need to be logged in to use the chatbot. Please log in first.