Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

NHibernate.ISession still open when using signalr #28

Open
MaxenceMouchard opened this issue May 30, 2023 · 9 comments
Open

NHibernate.ISession still open when using signalr #28

MaxenceMouchard opened this issue May 30, 2023 · 9 comments

Comments

@MaxenceMouchard
Copy link

MaxenceMouchard commented May 30, 2023

I integrated SignalR into my app some time after using your ISession injection implementation with Identity.

The problem is that signalr is connected with identity and ISession's injected in the identity implementation stay open.

So each Hub from SignalR Keep in Context identity and injected sessions are never kill.

So one session stay open for each Hub.

I was thinking to inject ISessionFactory in identity and open the sessions in the implemented methods for kills Sessions at each end of action but I do not know if this method is suitable, Knowing that the implementation of Users in UserStore is IQueryable so this would not be very safe to do something like:

private readonly ISessionFactory _sessionFactory;

public override IQueryable<User> Users
{
    get
    {
        using (ISession session = _sessionFactory.OpenSession())
        {
            return session.Query<User>();
        }
    }
}

public UserStore(ISessionFactory sessionFactory, IdentityErrorDescriber errorDescriber) : base(errorDescriber)
{
    _sessionFactory = sessionFactory;
}

what do you think about this case ?

Thank for your times.

@gliljas
Copy link
Member

gliljas commented May 30, 2023

Returning an IQueryable from within a "using session" doesn't work. What is the broader use case?

@MaxenceMouchard
Copy link
Author

MaxenceMouchard commented May 30, 2023

It works, but as I said, the fact that because it's queryable, my example is not safe and shouldn't be done, I'm aware of that, but I don't see any other options.

The broader use case is just an classique of SignalR.
When a hub is connected, it's look like the scopped Session from identity is never kill when an instance of the hub is created and when the Context.Identity is attached on the Hub.

Would you like an example git repo if I'm not clear ?

@gliljas
Copy link
Member

gliljas commented May 30, 2023

I don't see how it could work. The session is closed when the IQueryable is returned.

@MaxenceMouchard
Copy link
Author

MaxenceMouchard commented May 30, 2023

Don't get stuck on the Users property, it's not used in my implementation, it's just that Identity requires its implemation. It only works because Users is never called, but my other methods are implemented like this :

public override async Task<IdentityResult> CreateAsync(User user, CancellationToken cancellationToken = new CancellationToken()) {
    cancellationToken.ThrowIfCancellationRequested();
    ThrowIfDisposed();
    if (user == null) {
        throw new ArgumentNullException(nameof(user));
    }
    using(ISession session = _sessionFactory.OpenSession()) {
        await session.SaveAsync(user, cancellationToken);
        await FlushChangesAsync(session, cancellationToken);
    }
    return IdentityResult.Success;
}

public override async Task<IdentityResult> UpdateAsync(User user, CancellationToken cancellationToken = new CancellationToken()) {
    cancellationToken.ThrowIfCancellationRequested();
    ThrowIfDisposed();
    if (user == null) {
        throw new ArgumentNullException(nameof(user));
    }
    using(ISession session = _sessionFactory.OpenSession()) {
        var exists = await session.Query<User> ().AnyAsync(
            u => u.Id.Equals(user.Id),
            cancellationToken
        );
        if (!exists) {
            return IdentityResult.Failed(
                new IdentityError {
                    Code = "UserNotExist",
                        Description = $ "User with id {user.Id} does not exists!"
                }
            );
        }
        user.ConcurrencyStamp = Guid.NewGuid().ToString("N");


        await session.MergeAsync(user, cancellationToken);
        await FlushChangesAsync(session, cancellationToken);
        return IdentityResult.Success;
    }

}

public override async Task<IdentityResult> DeleteAsync(User user, CancellationToken cancellationToken = new CancellationToken()) {
    cancellationToken.ThrowIfCancellationRequested();
    ThrowIfDisposed();
    if (user == null) {
        throw new ArgumentNullException(nameof(user));
    }
    using(ISession session = _sessionFactory.OpenSession()) {

        await session.DeleteAsync(user, cancellationToken);
        await FlushChangesAsync(session, cancellationToken);
        return IdentityResult.Success;
    }
}

public override async Task<User?> FindByIdAsync(string userId, CancellationToken cancellationToken = new CancellationToken()) {
    cancellationToken.ThrowIfCancellationRequested();
    ThrowIfDisposed();
    var id = ConvertIdFromString(userId);

    using(ISession session = _sessionFactory.OpenSession()) {
        var user = await session.GetAsync<User> (id, cancellationToken);
        return user;
    }
}

@gliljas
Copy link
Member

gliljas commented May 30, 2023

That looks like a perfectly viable solution

@MaxenceMouchard
Copy link
Author

MaxenceMouchard commented May 30, 2023

It should work except if try to play with a user getting by FindByIdAsync function for example ...
So I agree it's viable but not really rustworthy/safe.
Have you ever had to deal with this issue with SignalR or do you have any advice or other more conveniant approach ?

@beginor
Copy link
Member

beginor commented May 31, 2023

If you are using SignalR Hubs to send message, please follow the rules of hubs https://learn.microsoft.com/en-us/aspnet/core/signalr/hubs?view=aspnetcore-7.0#create-and-use-hubs

Please Note

Hubs are transient:

  • Don't store state in a property of the hub class. Each hub method call is executed on a new hub instance.
  • Don't instantiate a hub directly via dependency injection. To send messages to a client from elsewhere in your application use an IHubContext.
  • Use await when calling asynchronous methods that depend on the hub staying alive. For example, a method such as Clients.All.SendAsync(...) can fail if it's called without await and the hub method completes before SendAsync finishes.

I think the safest useage should be like this:

public class MessageHub : Hub {

    public async Task SendMessage(string user, string message) {
        var factory = this.Context.GetHttpContext().RequestServices.GetService<ISessionFactory>();
        using var session = factory.OpenSession();
        var users = await session.Query<Users>().ToListAsync();
        await Clients.All.SendAsync("ReceiveMessage", user, users);
    }

}

Or try inject IServiceProvider in to you hub, like this:

public class MessageHub : Hub {

    private readonly IServiceProvider serviceProvider;

    public MessageHub(IServiceProvider serviceProvider) {
        this.serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
    }

    public async Task SendMessage(string user, string message) {
        var sessionFactory = serviceProvider.GetService<ISessionFactory>();
        using var session = sessionFactory.OpenSession();
        await Clients.All.SendAsync("ReceiveMessage", user, message);
    }

}

Not fully tested, just a suggestion;

@MaxenceMouchard
Copy link
Author

MaxenceMouchard commented May 31, 2023

Thank you for your advice but i don't use any injection or session inside my hubs. Hubs are clean as you defined the transiant.
My injection comes from UserStore which implements Identity and automatically binds with Signalr Hubs.

The problem is just that: Hub is automatically connected with identity and the session injected into the UserStore constructor stay open for the entire life of the application. So Each Hubs results in an additional open session.

@beginor
Copy link
Member

beginor commented Jun 2, 2023

By default UserStore and RoleStore are registered as Scoped (create instances per request), which is suitable for web api and mvc.

If this is not suitable for SignalR, you can register them with another life time. And this is easy, just dont use the default extension method AddHibernateStores , register them with another lifetime which are suiteable for SignalR.

PS: Any PR is welcome!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants