I'm trying to share the elements in cache between ServiceStack OOB ICacheClient and a SignalR Hub, but I'm getting the following error when I try to get the user session in the OnDisconnected event
Only ASP.NET Requests accessible via Singletons are supported
I have no issues accessing the session in the OnConnected event, so far this is what I've done:
public class HubService:Hub
{
private readonly IUserRepository _userRepository;
private readonly ICacheClient _cacheClient;
public HubService(IUserRepository userRepository,ICacheClient cacheClient)
{
_userRepository = userRepository;
_cacheClient = cacheClient;
}
public override System.Threading.Tasks.Task OnConnected()
{
var session = _cacheClient.SessionAs<AuthUserSession>();
//Some Code, but No error here
return base.OnConnected();
}
public override System.Threading.Tasks.Task OnDisconnected()
{
var session = _cacheClient.SessionAs<AuthUserSession>();
return base.OnDisconnected();
}
}
I'm using simple injector and my ICacheClient is registered as singleton:
Container.RegisterSingle<ICacheClient>(()=>new MemoryCacheClient());
the question is how do I register requests as singletons in SS? what am I missing on SignalR event?
Edit:
what I tried to expain for register requests in SS is because if there's a possibility to register SS IHttpRequest using a container and set the lifestyle as singleton due to the exception message, it seems like httpContext and IHttprequest are null by the OnDisconnected event
the SS code is the following:
public static string GetSessionId(IHttpRequest httpReq = null)
{
if (httpReq == null && HttpContext.Current == null)
throw new NotImplementedException(OnlyAspNet); //message
httpReq = httpReq ?? HttpContext.Current.Request.ToRequest();
return httpReq.GetSessionId();
}
what I'm trying to do is to store a list of connected users using ICacheClient and I just want to remove the connectionID from the list when a user get disconnected.
Edit:
it seems like according to danludwig post
"There is an interesting thing about SignalR... when a client disconnects from a
hub (for example by closing their browser window), it will create a
new instance of the Hub class in order to invoke OnDisconnected().
When this happens, HttpContext.Current is null. So if this Hub has any dependencies that are >registered per-web-request, something will probably go wrong."
the description above perfectly match my situation
I am no SingalR expert, but based on my experience with it and simple injector, I don't think you can get at Session (or Request, or HttpContext for that matter) during OnDisconnected. It kind of makes sense if you think about it though -- when a client disconnects from a hub, SignalR no longer has access to a session ID, there is no request, there is no more communication with the client. OnDisconnected is basically telling you "here, do something with this ConnectionId because the client it belonged to has gone away." Granted the user may come back, and then you can get access to the web goodies (session, request, etc, as long as you are IIS hosted) during OnReconnected.
I was having similar problems getting some simpleinjector dependencies to have correct lifetime scope during these 3 Hub connection events. There was 1 dependency that I wanted to register per http request, and it worked for everything except OnDisconnected. So I had to fenagle the SI container to use http requests when it could, but use a new lifetime scope when the dependency was needed during an OnDisconnected event. If you care to read it, I have a post here that describes my experiences. Good luck.
Related
I am trying to implement a multitenant (using a database per tenant) modular monolith that uses integration events to communicate between modules.
I am attempting to implement the integration events functionality and have the following scenario:
A new integration event is raised via an HTTP request
The integration event is published to the event bus
An integration event handler receives the message from the bus and modifies a domain object
The modification to the domain object causes a domain event to be raised
A domain event handler receives the domain event and performs some read operations on the database
This all works fine if I ignore the multitenant requirement, I am struggling to understand how to implement this for multiple tenants. I am using Finbuckle in my API and constructor injecting ITenantInfo into my DbContext to retrieve the correct connection string for the current tenant.
When I try to move the scenario to multitenant:
Step 1 is easy enough – I can simply pass a tenant identifier in the integration event.
Step 2 is unaffected – the message is published to the event bus.
Step 3 is tricky because it is asynchronous. If it was within the scope of the HTTP request, I would be able to inject the domain repository into the constructor of my integration event handler and retrieve the domain object from the database. However, because it is triggered from the event bus, the tenant identifier must be extracted from the message and manually resolved. Once I have resolved the tenant, I can manually create an instance of the domain repository and get the domain object from the database.
Step 4 is not affected; the domain object is updated as usual.
Step 5 is where I am struggling. I am constructor injecting the DbContext into the domain event handler which, before I introduced integration events, worked because ITenantInfo was resolved by the Finbuckle middleware and DI. The problem I have now is that I do not know how to resolve the tenant when the domain event is triggered via an integration event. My domain event does not contain a tenant identifier and there is no HttpContext therefore I cannot resolve ITenantInfo to provide the database connect string.
To clarify, the final step of my scenario requires the domain event handler to read data from the database via a DbContext. This is possible within the scope of an HTTP request because Finbuckle will resolve the ITenantInfo to provide the connection string. When outside the scope of an HTTP request, I do not know how to resolve the tenant because my domain event does not contain any tenant specific information.
How can I identify the tenant in a domain event handler when the domain event is raised as a side effect of an integration event?
For anybody that sees this in the future I was overthinking the problem.
I was able to resolve ITenantInfo using Scoped Filters in MassTransit.
For example:
public class MyTenantInfoConsumeFilter<T>
: IFilter<ConsumeContext<T>>
where T : class
{
readonly MyTenantInfo _tenantInfo;
private readonly MultiTenantStoreDbContext _dbContext;
public MyTenantInfoConsumeFilter(MyTenantInfo tenantInfo, MultiTenantStoreDbContext dbContext)
{
_tenantInfo = tenantInfo;
_dbContext = dbContext;
}
public Task Send(ConsumeContext<T> context, IPipe<ConsumeContext<T>> next)
{
if (context.Message is IntegrationEventBase message)
{
Map(_dbContext.TenantInfo.FirstOrDefault(tenant => tenant.Identifier == message.TenantIdentifier));
}
return next.Send(context);
}
public void Probe(ProbeContext context)
{
}
private void Map(MyTenantInfo? tenantInfo)
{
if (tenantInfo is null) return;
_tenantInfo.Id = tenantInfo.Id;
_tenantInfo.Identifier = tenantInfo.Identifier;
_tenantInfo.Name = tenantInfo.Name;
_tenantInfo.ConnectionString = tenantInfo.ConnectionString;
}
}
Then register the services using something like:
services.AddScoped<Finbuckle.MultiTenant.ITenantInfo>(sp => sp.GetService<MyTenantInfo>()!);
services.AddMassTransit(x =>
{
x.AddScoped<MyTenantInfo>();
x.UsingAzureServiceBus((context, cfg) =>
{
cfg.Host(hostContext.Configuration.GetConnectionString("AzureServiceBusConnection"));
cfg.ConfigureEndpoints(context);
cfg.UseConsumeFilter(typeof(MyTenantInfoConsumeFilter<>), context);
});
});
I am using hangfire and for authorization purposes, I am trying to access session value in Authorize([NotNull] DashboardContext context) method which is null. Please note that this problem has encountered after I have incorporated SignalR in my application.
If I remove the "app.MapSignalR();" line from startup.cs, I am able to access session successfully
public void Configuration(IAppBuilder app)
{
...
app.MapSignalR(); //if I remove this line, session is accessible
...
}
public class MyRestrictiveAuthorizationFilter : IDashboardAuthorizationFilter{
public bool Authorize([NotNull] DashboardContext context)
{
HttpSessionStateBase session = ((System.Web.HttpContextWrapper)(owinEnvironment["System.Web.HttpContextBase"])).Session; //Which is null
session = HttpContext.Current.Session; //Also Null
}
}
Please note that session value is null after I introduced " app.MapSignalR()" in Configuration method in startup.cs
According to ASP.Net Core Docs, getting null value of session is normal behaviour.
Session isn't supported in SignalR apps because a SignalR Hub may execute independent of an HTTP context. For example, this can occur when a long polling request is held open by a hub beyond the lifetime of the request's HTTP context.
Also
SignalR and session state
SignalR apps should not use session state to store information. SignalR apps can store per connection state in Context.Items in the hub.
Docs
It seems like a big use for SignalR Hubs is to display the actions of one client to all of the other clients. What I hope to use SignalR for is when a certain event happens in my server side code, I want to instantiate a hub object and invoke one of its methods to communicate with all of the clients. If you see my previous post (Route To Take With SqlDependency OnChange), I would like to do this in the OnChange method of SqlDependency. Upon researching it I have found some people talk about using an IHubContext object, though I haven't found many examples of instantiation and actual sending data to clients.
Is this possible to do (and what might sending data to all clients with IHubContext look like if possible), and if not, are there any ways I might be able to get around instantiating a hub like this?
SignalR for ASP.NET Core
You can create a class that has the IHubContext<T> injected in. Inject other dependencies if you want, or resolve the service from controllers or other classes.
public class NotificationService
{
private readonly IHubContext<MyHub> _myHubContext;
public NotificationService(IHubContext<MyHub> myHubContext)
{
_myHubContext= myHubContext;
}
public async Task SendMessage(string message)
{
await _myHubContext.Clients.All.SendAsync("Update", message);
}
}
Assuming you're using SqlDependency from an IHostedService:
public class MyHostedService : IHostedService
{
public MyHostedService(
NotificationService notificationService)
{
// TODO get reference to sqlDependency
sqlDependency.OnChange += (s, e) => _notificationService.SendMessage(e.Info.ToString());
}
}
SignalR for ASP.NET
var context = GlobalHost.ConnectionManager.GetHubContext<MyHub>();
context.Clients.All.sendMessage(message);
You need to use using Microsoft.AspNet.SignalR library.
using Microsoft.AspNet.SignalR;
//Instantiating. SignalRHub is the hub name.
var context = GlobalHost.ConnectionManager.GetHubContext<SignalRHub>();
//sends message
context.Clients.Client(ClientId).sendMessage(data);
I'm in a situation where the classic functionality of vnext's DI container is not enough to provide me with the correct functionality. Let's say I have a DataService that gets data from a database like this:
public class DataService : IDataService, IDisposable {
public List<MyObject> GetMyObjects()
{
// do something to fetch the data...
return myObjects;
}
}
I can then register this service in the DI container during the configuration phase in Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
services.AddScoped(typeof(IDataService), typeof(DataService));
}
This ensures the correct lifecylce of the service (one per request scope), however, I need the service to access a different database when a different request is made. For simplicity reasons, let's say the following scenario applies:
when a request to my Web API is made, the DataService will access the currently logged in user, which contains a claim called Database which contains the information which database to use.
the DataService is then instantiated with the correct database connection.
In order to get the second step to work, I have created a constructor for the DataService like this:
public DataService(IHttpContextAccessor accessor)
{
// get the information from HttpContext
var currentUser = accessor.HttpContext.User;
var databaseClaim = currentUser.Claims.SingleOrDefault(c => c.Type.Equals("Database"));
if (databaseClaim != null)
{
var databaseId = databaseClaim.Value;
// and use this information to create the correct database connection
this.database = new Database(databaseId);
}
}
By using the currently logged in user and his claims, I can ensure that my own authentication middleware takes care of providing the necessary information to prevent attackers from trying to access the wrong database.
Of course adding the IDisposable implementation is required to cleanup any database connections (and gets called correctly using the scope lifecycle).
I can then inject the DataService into a controller like this
public MyController : Controller
{
private IDataService dataService;
public MyController(IDataService dataService)
{
this.dataService = dataService;
}
}
This all works fine so far.
My questions now are:
Is there another way to create the instance other than using the constructor of the DataService? Maybe accessing the object the IServiceCollection provides in a different place other than during the configration phase which runs only once? Maybe using my own OWIN middleware?
Is this method really safe? Could two requests made at the same time accidentally end up with the DataServiceintended for the other request and therefore end up giving out the wrong data?
What you have is fine.
Is there another way to create the instance other than using the constructor of the DataService? Maybe accessing the object the IServiceCollection provides in a different place other than during the configration phase which runs only once? Maybe using my own OWIN middleware?
Not really. You can use delegate registration but it's the same problem.
Is this method really safe?
Yes
Could two requests made at the same time accidentally end up with the DataServiceintended for the other request and therefore end up giving out the wrong data?
Nope. The IHttpContextAcessor uses AsyncLocal (http://blog.stephencleary.com/2013/04/implicit-async-context-asynclocal.html) to provide access to the "current" http context.
In my MVC application, I user SignalR for communication between users. Basically, a client calls a method on the hub, which calls a method on a repository, which then saves the message to the database and the hub notifies the other client of the new message.
I had used the GetOwinContext() method during these calls from the client to get the current instance of UserManager and ApplicationDbContext, by using the GetUserManager<UserManager>() and Get<ApplicationDbcontex>() extension methods, respectively. However, I have noticed that calls from the same connection use the same context, which is, obviously, not a very good thing. I went ahead and changed my repository so it is like this now:
public XyzRepository() //constructor
{
db = ApplicationDbContext.Create(); //static method that generates a new instance
}
private ApplicatonDbContext db { get; set; }
private UserManager UserManager
{
get
{
return new UserManager(new UserStore<ApplicationUser>(db)); //returns a new UserManager using the context that is used by this instance of the repository
}
}
Since I reference the ApplicationUser objects using the UserManager (using FindByIdAsync(), etc, depending on the design), it is extremely important to use the context I currently work with for the UserStore of the UserManager's current instance. The repository is created once per request, which seems to apply to each SignalR calls as intended. While I have experienced no problems with this design so far, after reading about the issue (in this article), particularly this line:
"In the current approach, if there are two instances of the UserManager in the request that work on the same user, they would be working with two different instances of the user object.", I decided to ask the community:
Question: what is the preferred way to use ASP.NET Identity's UserManager class with SignalR, if it is imperative that I use the same instance of DbContext for my repository's methods that the UserManager's UserStore uses?
I think the preferred way is to use an Inversion of Control container and constructor-inject dependencies with some kind of lifetime scope. Here is another question that you might want to look into:
Using Simple Injector with SignalR
It is preferable that your DbContext instance live as long as the current web request. IoC containers have facilities that let you register DbContext instances with per web request lifetimes, but you need to set up the IoC container so that it can manage the construction of the Hub classes to achieve this. Some IoC containers (like SimpleInjector) will also automatically dispose of the DbContext at the end of the web request for you, so you don't need to wrap anything in a using block.
As for the UserManager, XyzRepository, etc, I think those can also have per-web-request lifetime, or even transient lifetimes. Ultimately, I don't see why you wouldn't be able to achieve something like this:
public class MyXyzHub : Hub
{
private readonly UserManager<ApplicationUser> _userManager;
private readonly MessageRepository _messageRepository;
public MyXyzHub(UserManager<ApplicationUser> userManager,
MessageRepository messageRepository)
{
_userManager = userManager;
_messageRepository= messageRepository;
}
public void sendMessage(string message)
{
var user = _userManager.FindByIdAsync(...
_messageRepository.CreateAndSave(new Message
{
Content = message, UserId = user.Id
});
Clients.All.receiveMessage(message, user.Name);
}
}
If you wire up your IoC container the right way, then every time the Hub is constructed, it should reuse the same ApplicationDbContext instance for the current web request. Also with your current code, it looks like XyzRepository is never disposing of your ApplicationDbContext, which is another problem that an IoC container can help you out with.