- 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)
-
0
I will check it
-
0
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 }); } } }
-
0
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.
-
0
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.
-
0
Hi,
Can you share the test project with me? I will check it. my email is shiwei.liang@volosoft.com
-
0
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
- Create a record with shared db (By clicking the first button)
- 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.
-
0
Hi,
The ZEntityFramework not working with EF Core entity tracking. this is not related to ABP.
You need to publish the event manually
-
0
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?
-
0
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
-
0
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.