Starts in:
1 DAY
8 HRS
0 MIN
20 SEC
Starts in:
1 D
8 H
0 M
20 S
Open Closed

AddLocalEvent not publishing events when tenant has separate db. #6032


User avatar
0
cangunaydin created
  • ABP Framework version: v7.4.0
  • UI Type: Angular
  • Database System: EF Core ( PostgreSQL)
  • Tiered (for MVC) or Auth Server Separated (for Angular): yes

Hello I want to serve different databases for different tenants and I know abp supports that and have a very good documentation about it. In my case i have a little bit different scenario.

I am building a server that multiple iot devices will hit the server and they are gonna be registered to host database (they do not belong to any tenant at that moment). Then the host tenant administrator needs to assign the devices to one of the tenants in the system from user interface.

So when it is assigned, i need to check if tenant has a separate db connection and according to that i will change the ICurrentTenant and insert a new data to tenant db. Then delete the data from host db. That is my plan. Sth like this.

    public async Task ApproveForSeparateDbAsync(ApproveScreenForSeparateDbDto input)
    {
        var newScreens = new List<Screen>();
        var screens = await _screenRepository.GetListByIdsAsync(input.ScreenIds);
        using (CurrentTenant.Change(input.TenantId))
        {
            foreach (var screen in screens)
            {
                var newScreen = new Screen(GuidGenerator.Create(),
                    screen.Name,
                    screen.MacAddress,
                    screen.DeviceType,
                    screen.DeviceTypeVersion,
                    screen.ModelNumber,
                    screen.ApplicationVersion,
                    screen.IpAddress,
                    screen.Port,
                    screen.CurrentClock,
                    input.TenantId);
                newScreen.Approve(input.TenantId,isSeparateDb:true);
                newScreens.Add(newScreen);
             
            }
            await _screenRepository.InsertManyAsync(newScreens);
            
        }
        foreach (var screen in screens)
        {
            await _screenManager.DeleteWithEventAsync(screen);
        }
    }

Screen is a FullAuditedAggregateRoot with IMultiTenant interface.

here is screen constructor.

 public Screen(
        Guid id,
        string name,
        string macAddress,
        string deviceType,
        int deviceTypeVersion,
        string modelNumber,
        string applicationVersion,
        string ipAddress,
        int port,
        DateTime currentClock,
        Guid? tenantId = null) : base(id)
    {
        Check.NotNullOrWhiteSpace(macAddress, nameof(macAddress));
        Check.NotNullOrWhiteSpace(deviceType, nameof(deviceType));
        Check.NotNullOrWhiteSpace(modelNumber, nameof(modelNumber));
        Check.Positive(deviceTypeVersion, nameof(deviceTypeVersion));

        TenantId = tenantId;
        UpdateName(name);
        MacAddress = macAddress;
        DeviceType = deviceType;
        DeviceTypeVersion = deviceTypeVersion;
        ModelNumber = modelNumber;
        UpdateApplicationVersion(applicationVersion);
        CurrentClock = currentClock;
        IpAddress = ipAddress;
        Port = port;
        var openingHours = OpeningHoursHelper.GetDefault().Select(o => new OpeningHoursEto()
        {
            Id = o.Id,
            Day = o.Day,
            StartTime = o.StartTime,
            EndTime = o.EndTime
        }).ToList();
        AddLocalEvent(new ScreenCreatedEto()
        {
            Id = id,
            Name = name,
            MacAddress = macAddress,
            TenantId = TenantId,
            OpeningHours = openingHours
        });
    }

you can see that over here i am trying to trigger an event so listeners can do the necessary work. when unitofwork completed it doesn't trigger any event. Weird part is if i do the same by injecting ILocalEventBus to appservice. It triggers the event. and you can see the local events on CurrentUnitOfWork.LocalEventHandlers.

Also if i change

using (CurrentTenant.Change(input.TenantId))

to

using (CurrentTenant.Change(null))

it also triggers the events. I suppose this is some kind of bug or sth i don't know when the current tenant has different Database.


10 Answer(s)
  • User Avatar
    0
    liangshiwei created
    Support Team Fullstack Developer

    I will check it

  • User Avatar
    0
    liangshiwei created
    Support Team Fullstack Developer

    Hi,

    I could not reproduce the problem, could you provide the full steps to reproduce? thanks.

    public class MyEventData : IMultiTenant
    {
        public MyEventData(string name, Guid? tenantId)
        {
            Name = name;
            TenantId = tenantId;
        }
        public string Name { get; set; }
        public Guid? TenantId { get; }
    }
    
    public class MyEventHandler : ILocalEventHandler<MyEventData>, ITransientDependency
    {
        private readonly ILogger<MyEventHandler> _logger;
    
        public MyEventHandler(ILogger<MyEventHandler> logger)
        {
            _logger = logger;
        }
    
        public Task HandleEventAsync(MyEventData eventData)
        {
            _logger.LogInformation("MyEventData: {0}", eventData.Name);
            return Task.CompletedTask;
        }
    }
    
    public class Book : FullAuditedAggregateRoot<Guid>,  IMultiTenant
    {
        public Guid? TenantId { get; }
    
        public string Name { get; set; }
        
        
    
        public Book(Guid id, string name, Guid? tenantId)
        {
            Id = id;
            Name = name;
            TenantId = tenantId;
            
            AddLocalEvent(new MyEventData(name, tenantId));
        }
    }
    
    public class TestAppService : Myapp4AppService
    {
        private readonly IRepository<Book, Guid> _bookRepository;
    
        public TestAppService(IRepository<Book, Guid> bookRepository)
        {
            _bookRepository = bookRepository;
        }
    
        public async Task Test(Guid tenantId)
        {
            using (CurrentTenant.Change(tenantId))
            {
                var book = new Book(GuidGenerator.Create(), "test", tenantId);
                var book2 = new Book(GuidGenerator.Create(), "test2", tenantId);
    
                await _bookRepository.InsertManyAsync(new[] { book, book2 });
            }
        }
    }
    

  • User Avatar
    0
    cangunaydin created

    Hello, I tried to create a sample app, at first as you say it works fine. here is the appservice that i have used.

     public async Task CreateForSeparateDb(Guid bookId, Guid tenantId)
     {
         var book = await _bookRepository.GetAsync(bookId);
         var tenantBooks=new List<Book>();
         using (CurrentTenant.Change(tenantId))
         {
             var tenantBook = new Book(GuidGenerator.Create(), book.Name + " Tenant", tenantId); //adding local event in book aggregate root.
             tenantBooks.Add(tenantBook);
    
             await _bookRepository.BulkInsertAsync(tenantBooks);
         }
         await _bookRepository.DeleteAsync(book);
         await _localEventBus.PublishAsync(new BookDeletedEto()
         {
             Id = book.Id,
             TenantId = null
         });
    
     }
    

    Then when i go through my code, i realized that I am doing bulkinsert. I use ZEntityFramework Extensions nuget package. I named my method same as default abp repositories method (InsertManyAsync()) so i couldn't see it at first sight. Here is the code for my repository method.

            public async Task BulkInsertAsync(List<Book> books,CancellationToken cancellationToken = default)
            {
                DbContext dbContext = (DbContext)(await GetDbContextAsync());
                books.ForEach(o => { o.CreationTime = _clock.Now; });
                await dbContext.BulkInsertAsync(books,
                    cancellationToken: GetCancellationToken(cancellationToken),
                    options:
                    (operation) => operation.BatchSize = 1000);
            }
    

    so dbcontext is coming right

    The behavior is kind of confusing here. when i bulk insert with current tenant equals null, local events are triggered. When I change the current tenant to separate db tenant, it doesn't trigger for the entity that i create but instead it triggers for the entity that is already in host db with tenantid null.

    after insert what i got in event handler.

    I couldn't understand from where abp is trying to find the local events. When i look at the code it seems like it is trying to get the changed entities, and getting the local events from that entity.

    maybe ZEntityFramework Extensions is not marking the entities as modified but if it is like that why it works on currentTenant=null scenario?

    I can send you the sample app that i have created if you want.

  • User Avatar
    0
    cangunaydin created

    and as a note, while i am creating a separate db from ui, i find out another thing. maybe this should be another ticket but i want to mention over here. look at the image below.

    when you create new tenant if you are not using shared db and want to specify connection string for the module, it throws an exception as you can see in the image. Model validation is expecting extra parameters. This comes from the SaasModule.

    If you create with shared db and edit the connection strings after the creation, it works.

  • User Avatar
    0
    liangshiwei created
    Support Team Fullstack Developer

    Hi,

    Can you share the test project with me? I will check it. my email is shiwei.liang@volosoft.com

  • User Avatar
    0
    cangunaydin created

    Hello, I have sent you the email. As a note, I have created two buttons on host dashboard page. One for creating a record with shared db, and the other one is with separate db. You can use them if you want.

    To reproduce the issue

    1. Create a record with shared db (By clicking the first button)
    2. Then move that record from shared db to separate db (By clicking the second button)

    I have also created a separate module in the project. So it could resemble what i want to achieve in my real project. Thank you for the help.

  • User Avatar
    0
    liangshiwei created
    Support Team Fullstack Developer

    Hi,

    The ZEntityFramework not working with EF Core entity tracking. this is not related to ABP.

    You need to publish the event manually

  • User Avatar
    0
    cangunaydin created

    I see. But interesting thing is, when i save through ZEntityFramework BulkInsert method, it triggers the event for previous record. that is already in db. Why it behaves like that?

    So publishing event manually is not gonna work since the same event will be triggered twice one for previous record and one for new record. Instead it should be triggered once only for the new record.

    It seems like, i shouldn't use ZEntityFramework if Aggregate Root have a local event that should have been triggered afterwards. Is there any alternatives that i can use for bulk operations that will trigger the event.

    Bulk operations for abp framework (https://docs.abp.io/en/abp/7.4/Repositories#bulk-operations). Is it adjusted for performance? or it is just an extension for not to do the loop?

  • User Avatar
    0
    liangshiwei created
    Support Team Fullstack Developer

    Hi,

    So publishing event manually is not gonna work since the same event will be triggered twice one for previous record and one for new record. Instead it should be triggered once only for the new record.

    Because you added an event to the constructor, there will always be an event here.

    You can consider adding a method, for example:

    public class Book : FullAuditedAggregateRoot<Guid>, IMultiTenant
    {
        public string Name { get; private set; }
        public Guid? TenantId { get; private set; }
    
        public Book(Guid id, string name, Guid? tenantId) : base(id)
        {
            Check.NotNull(name, nameof(name));
            Name= name;
            TenantId = tenantId;
        }
    
        public static Book Create(Guid id, string name, Guid? tenantId)
        {
            var book = new Book(id, name, tenantId);
            book.AddLocalEvent(new BookCreatedEto()
            {
                Id = id,
                Name = name,
                TenantId = tenantId
            });
    
            return book;
        }
    }
    

    Bulk operations for abp framework (https://docs.abp.io/en/abp/7.4/Repositories#bulk-operations). Is it adjusted for performance? or it is just an extension for not to do the loop?

    The ZEntityFramework is the pro library, It will have better performance. ABP uses the EF Core native API: https://learn.microsoft.com/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontext.addrangeasync?view=efcore-7.0

  • User Avatar
    0
    cangunaydin created

    ok it is clear now, thank you for your time i will implement your solution i guess. Or switch to ef core native. But thanks for the help.

Made with ❤️ on ABP v9.1.0-preview. Updated on November 20, 2024, 13:06