Search code examples
signalrabp-framework

(ABP/SignalR) Database operations not applied from SignalR Hub


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:

  1. Is this behavior to be expected / normal / supposed to be this way?
  2. Is there a more elegant (or, elegant at all :) ) way to call a RESTful API from a SignalR hub and apply database operations?

I have created a github repository to showcase this. It is just the basic ABP.io BookStore tutorial project with SignalR implemented:

Acme.BookStore

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:

  1. (Run DbMigrator to create database and initial migration)
  2. Open Books view (Book Store -> Books)
  3. New Book
  4. Fill out form
  5. Save

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.


Solution

  • 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);
        }
    }