Using SignalR for a ABP web app, any database operations which originate from within a SignalR hub are not actually applied to the database. This can be circumvented by overriding the hub's OnConnectedAsync virtual and querying the database in any form, using a UnitOfWork instance. I suspect this has something to do with SignalR holding an ongoing connection between client(s) and the hub, and this connection not being closed which prevents the application of database operations.
Questions:
I have created a github repository to showcase this. It is just the basic ABP.io BookStore tutorial project with SignalR implemented:
public class BookHub : AbpHub<IBookHub>
{
public async override Task OnConnectedAsync()
{
IUnitOfWorkManager uowm = LazyServiceProvider.LazyGetRequiredService<IUnitOfWorkManager>();
using (IUnitOfWork uow = uowm.Begin())
{
IRepository<Book, Guid> bookRepository = uow.ServiceProvider.GetRequiredService<IRepository<Book, Guid>>();
if (await bookRepository.AnyAsync() == false)
{
throw new EntityNotFoundException(typeof(Book));
}
}
await base.OnConnectedAsync();
}
[HubMethodName("CreateOrUpdateBook")]
public async Task CreateOrUpdateBookAsync(CreateUpdateBookDto createDto, Guid? id)
{
IBookAppService bookAppService = LazyServiceProvider.LazyGetRequiredService<IBookAppService>();
BookDto bookDto = id.HasValue ? await bookAppService.UpdateAsync((Guid)id!, createDto) : await bookAppService.CreateAsync(createDto);
await Clients.All.BookUpdated(bookDto);
}
}
ABP Framework version: 8.0.1
Database provider: EF Core
Steps needed to reproduce the problem:
The new book that was supposed to be inserted into the database is returned to the frontend, but the database has only been actually changed because the book database was queryied once before in BookHub's OnConnectedAsync. Without that initial query, the changes to the database will not be applied.
A helpful user "realLiangshiwei" over at the abp github provided a solution. https://github.com/abpframework/abp/issues/18793#issuecomment-1906049286
An IHubFilter is added to the HubOptions during configuration (depending on the way your solution is structured, this could be done inside ProtocolsHttpApiHostModule.cs). This makes the UnitOfWork call CompleteAsync after a task is done and thereby avoid locking the database.
public class AbpUnitOfWorkHubFilter : IHubFilter
{
public virtual async ValueTask<object?> InvokeMethodAsync(HubInvocationContext invocationContext, Func<HubInvocationContext, ValueTask<object?>> next)
{
var unitOfWorkManager = invocationContext.ServiceProvider.GetRequiredService<IUnitOfWorkManager>();
using (var uow = unitOfWorkManager.Reserve(UnitOfWork.UnitOfWorkReservationName, requiresNew:true))
{
var result = await next(invocationContext);
await uow.CompleteAsync();
return result;
}
}
}
Configure<HubOptions>(options =>
{
options.AddFilter<AbpUnitOfWorkHubFilter>();
});
public class BookHub : AbpHub<IBookHub>
{
[HubMethodName("CreateOrUpdateBook")]
public virtual async Task CreateOrUpdateBookAsync(CreateUpdateBookDto createDto, Guid? id)
{
IBookAppService bookAppService = LazyServiceProvider.LazyGetRequiredService<IBookAppService>();
BookDto bookDto = id.HasValue ? await bookAppService.UpdateAsync((Guid)id!, createDto) : await bookAppService.CreateAsync(createDto);
await Clients.All.BookUpdated(bookDto);
}
}