I have introduced SignalR into my ASP.NET Core 2 project, but I'm having some issues using a scoped service that I normally use in my controllers. I feel like the problem may be due to the difference in lifecycles between HTTP requests, websockets and hubs.
On each HTTP request, middleware reads the Authorization token and updates some properties (e.g. id, claims, etc.) on a scoped service (IUser) for the request. I use this service in all of my controllers with no issue. To get this to work with SignalR, I am sending an access_token query parameter and using some other middleware beforehand to add this query parameter as a header which works fine.
The problem arises when I am trying to access the IUser service in my SignalR hub. On construction of the hub, the IUser that gets injected has none of the properties set, despite the middleware for the /hub request just setting them.
If I set the service to be a singleton then it works, but IUser should never persist longer than a single request.
How should I go about setting an IUser for a specific SignalR connection?
// Startup.cs - User has several settable properties
services.AddScoped<IUser, User>();
// User Middleware
public class UserMiddleware
{
private readonly RequestDelegate _next;
public UserMiddleware(RequestDelegate next)
{
_next = next;
}
public Task Invoke(HttpContext context)
{
// retrieve service for this request
var user = context.RequestServices.GetService<IUser>();
// set some properties on the User from auth token
// e.g. User.Id = 1;
return _next(context);
}
}
[Authorize(Roles = Claim.Names.Read)]
public class MyHub : Hub
{
private readonly IUser _user;
public MyHub(IUser user)
{
// user that gets injected has null properties
_user = user;
}
public async Task Foo()
{
// do work with the _user credentials
}
}
public class DownloadController : Controller
{
private readonly IUser _user;
public DownloadController(IUser user)
{
// user gets injected and has properties set
_user = user;
}
}
Have you tried Context.GetHttpContext().RequestServices in your Hub-method? That should be the IServiceProvider of the ongoing request.
To use scope service in signalr hub, you could inject ServiceProvider and create a scope in it directly:
public class ChatHub : Hub
{
private IServiceProvider _serviceProvider;
public ChatHub(IServiceProvider serviceProvider)
{
_serviceProvider= serviceProvider;
}
public async Task Foo()
{
using (var scope = _serviceProvider.CreateScope())
{
var dbContext = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
var user = scope.ServiceProvider.GetRequiredService<IUser>();
}
}
}
Related
I have a .NET Core 3.1 Web API application that connects to different databases depending on what organization the user is from.
The organization is stored as a string value in their user account in Azure AD B2C, and upon performing a request, I need that user's organization to be identified and the correct connection string chosen based on their organization.
However, I'm having trouble getting the user's 'userId' in my DbContext.
I have tried two different implementations but neither are working.
Within the 'ConfigureServices' method in my Startup.cs file I have specified the following:
...
services.AddHttpContextAccessor();
services.AddTransient<IUserRepository, UserRepository>();
I then declared the Interface with its Service:
public interface IUserRepository
{
public void LogCurrentUser();
}
public class UserRepository : IUserRepository
{
private readonly IHttpContextAccessor _httpContextAccessor;
public UserRepository(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
public void LogCurrentUser()
{
var username = _httpContextAccessor.HttpContext.User.Identity.Name;
}
}
3)And finally, on my DbContext, I attempt to draw the userId from the claim:
public class ApplicationDbContext : DbContext
{
private readonly ApplicationTenantClient _tenant;
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options,
ITenantService tenantProvider,
IUserRepository userRepository,
IHttpContextAccessor httpContextAccessor) : base(options)
{
// Attempt (1)
userRepository.LogCurrentUser();
// Attempt (2)
var userAccessor = httpContextAccessor.HttpContext.User.FindFirst(ClaimTypes.NameIdentifier);
if(userAccessor != null)
{
var userId = userAccessor.Value;
}
_tenant = tenantProvider.GetTenant(); // <- ideally send userId here for lookup on AD Graph
}
...
After authenticating the user, then attempting to call an endpoint, the claim is empty:
First Result:
Second Result:
This is.. highly confusing, any help would be appreciated.
Articles that I have attempted to follow
https://learn.microsoft.com/en-us/aspnet/core/fundamentals/http-context?view=aspnetcore-5.0
https://www.koskila.net/how-to-get-current-user-in-asp-net-core/
I have requirement to add in a header on each request to a service a header MyHeader. MyHeader is the jwt I have received when the user is logging on.
I tried to read it from HttpContext.Request.Headers.
I am trying to access it in my service. I could get the result in the controller but not on the Service layer. Can anyone help me to get the same on service class.
I'm using Asp.net core
In Startup#ConfigureServices:
services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
In your service class:
public class YourServiceClass
{
private readonly IHttpContextAccessor _httpContextAccessor;
...
// constructor
public YourServiceClass(IHttpContextAccessor httpContextAccessor, ...)
{
_httpContextAccessor = httpContextAccessor;
...
}
public void YourServiceMethod()
{
var headers = _httpContextAccessor.HttpContext.Request.Headers;
...
}
...
On an ASP.NET Core 2.2 controller I have the following:
var principal = this.User as ClaimsPrincipal;
var authenticated = this.User.Identity.IsAuthenticated;
var claims = this.User.Identities.FirstOrDefault().Claims;
var id = this.User.FindFirstValue(ClaimTypes.NameIdentifier);
I am able to check if the user is authenticated and gets the claims including id.
How can I do the same outside of the Controller where I do not have this.User?
Inject IHttpContextAccessor interface into the target class. This will give access to the current User via the HttpContext
This provides an opportunity to abstract this feature by creating a service to provide just the information you want (Which is the current logged in user)
public interface IUserService {
ClaimsPrincipal GetUser();
}
public class UserService : IUserService {
private readonly IHttpContextAccessor accessor;
public UserService(IHttpContextAccessor accessor) {
this.accessor = accessor;
}
public ClaimsPrincipal GetUser() {
return accessor?.HttpContext?.User as ClaimsPrincipal;
}
}
You need to setup IHttpContextAccessor now in Startup.ConfigureServices in order to be able to inject it:
services.AddHttpContextAccessor();
services.AddTransient<IUserService, UserService>();
and inject your service where needed.
It's important to note that HttpContext could be null. Just because
you have IHttpContextAccessor, doesn't mean that you're going to
actually be able to always get the HttpContext. Importantly, the code where
you're using this must be within the request pipeline in some way or
HttpContext will be null.
Credit #ChrisPratt via comment
I have my own simple framework for routing/controllers in C# and .NET Core. I'm using EF Core for the ORM. In Startup.cs I'm configuring it like so:
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<Context>(options =>
{
options.EnableSensitiveDataLogging();
options.UseSqlServer(System.Environment.GetEnvironmentVariable("SQL_SERVER_CONNECTION"));
});
}
I'm using dependency injection to get an instance of my DbContext. In my controller actions I do the following:
Action 1:
Do not use AsNoTracking() on my queries
Make changes to a model instance
Do not save changes
Action 2 (another HTTP request):
Do literally anything
Run SaveChangesAsync()on DbContext
The changes made in Action 1 are then persisted. If I severed any relations in Action 1 then I get an error.
I know that by default DbContext is scoped. Do I have to implement some of my own scoping code to ensure that I get a new instance with each HTTP request?
NOTE: I am NOT using MVC, I am using my own little library that I'm developing. I just learned that MVC probably uses IServiceScopeFactory to generate scopes. I am not sure how to use it in middleware though.
I got it. Here's how to wrap a scope with HttpContext in a middleware:
public class Middleware
{
private readonly RequestDelegate _next;
private IServiceScopeFactory _scopeFactory;
public Middleware(RequestDelegate next, IServiceScopeFactory scopeFactory)
{
_next = next;
_scopeFactory = scopeFactory;
}
public async Task Invoke(HttpContext context)
{
using (var scope = _scopeFactory.CreateScope())
{
context.RequestServices = scope.ServiceProvider;
// Do whatever you want here
if (_next != null)
await _next(context);
}
}
}
I see a lot of code examples on how to use DI in .NET Core, however none of them use constructor parameters.
For example:
Create Authorization Service
Inject the current HTTP header(X-Api-Key) in constructor
In the implementation check if I have access
Here I need to not only use DI on my IAuthorizationService but also inject the token in the constructor. I know how to do it in Ninject, however have no experience in .NET Core DI.
Here is what I have as an example.
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddEntityFrameworkSqlite();
services.AddDbContext<MainDbContext>();
services.AddScoped<IAuthorizationService, AuthorizationService>(); // Inject current HttpContext header value as a constructor?
}
I usually flow such values through a service where the data is set in a piece of middleware. For example:
An accessor class which can be injected:
public class ApiKeyAccessor
{
public string ApiKey { get; set; }
}
And a middleware which sets the API key at the beginning of the request:
public class ApiKeyMiddleware
{
private readonly RequestDelegate _next;
public ApiKeyMiddleware(RequestDelegate next)
{
_next = next;
}
public Task Invoke(HttpContext context, ApiKeyAccessor apiKeyAccessor)
{
StringValues key;
if (context.Request.Headers.TryGetValue("X-Api-Key", out key))
{
apiKeyAccessor.ApiKey = key;
return _next(context);
}
// todo: throw exception, etc..
}
}
Now all we have to is add the ApiKeyAccessor to the DI container with a scoped lifetime and add the ApiKeyMiddleware to the request execution pipeline, preferably as soon as possible.
When configured correctly, we can inject the ApiKeyAccessor instance in controllers or services:
public class AuthorizationService
{
private readonly string _apiKey;
public AuthorizationService(ApiKeyAccessor apiKeyAccessor)
{
_apiKey = apiKeyAccessor.ApiKey;
}
}