I'm using ASP.NET Core 1.1.0 and EF Core 1.1.0. I extended the built-in JWT Bearer authentication with my own middleware, but data is being cached between calls. I have a feeling the DB context is being reused, but I'm not sure how to troubleshoot.
Specifically, I'm calling the token endpoint to invoke the middleware and get a new JWT. The user is returned, all is good. If I manually update the DB with SSMS then log in again, the old, outdated user information is returned. The DB is being hit both times and it's returning the correct data (I verified with SQL Profiler), but EF is apparently ignoring the updated data in favor of what it already had.
I'm using standard DI with the token middleware, nothing special there. Relevant code is below.
Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<MyContext>(options =>
options.UseSqlServer(Environment.GetEnvironmentVariable("DEV_CONNECTIONSTRING")));
services.AddScoped<UserService>();
}
public void Configure(IApplicationBuilder app,
IHostingEnvironment env,
ILoggerFactory loggerFactory,
IApplicationLifetime appLifetime)
/* These options specify:
* - token endpoint
* - authentication scheme
* - JWT issuer
* - duration of JWT validity
* - duration of refresh token validity */
app.UseMiddleware<JwtProviderMiddleware>(new JwtProviderOptions(
"/api/users/token",
"Bearer",
"MyJwtIssuer",
Duration.FromMinutes(30),
Duration.FromDays(7)
));
}
Custom JWT provider middleware:
public class JwtProviderMiddleware
{
readonly RequestDelegate _next;
readonly JwtProviderOptions _options;
readonly UserService _userSvc;
public JwtProviderMiddleware(RequestDelegate next,
JwtProviderOptions options,
UserService userSvc)
{
_next = next;
_options = options;
_userSvc = userSvc;
}
public async Task Invoke(HttpContext context)
{
// parse request body for email address and password
// user is returned incorrectly here
var user = await _userSvc.GetByEmailAsync(email);
}
}
UserService:
public class UserService
{
MyContext _context;
public UserService(MyContext context)
{
_context = context;
}
public async Task<User> GetByEmailAsync(string email)
{
return await _context.Users.FirstOrDefaultAsync(u => u.Email == email);
}
}
EF Core version: 1.1.0
Database Provider: Microsoft.EntityFrameworkCore.SqlServer
Operating system: Windows 10 Education x64
IDE: VS 2015 Enterprise
@vaindil The DbContext is registered as scoped, which means that:
@ajcvickers The UserService is scoped, so is the problem that the middleware is (I assume) a singleton? If that's the case I can try to figure that out and close this issue as it's not an EF problem.
@vaindil sounds about right; you can grab a scoped instance of UserService from HttpContext.RequestServices for a quick solution. I'm sure there are 'better' ways to do it with constructor injection and all, though.
@tuespetre Perfect, that's what I needed. Startup.cs stays the same, but I had to change the middleware to the format below. I made _userSvc not readonly and removed it from the constructor, then set it at the top of Invoke() using context.RequestServices.
Thank you!
public class JwtProviderMiddleware
{
readonly RequestDelegate _next;
readonly JwtProviderOptions _options;
UserService _userSvc;
public JwtProviderMiddleware(RequestDelegate next,
JwtProviderOptions options)
{
_next = next;
_options = options;
}
public async Task Invoke(HttpContext context)
{
_userSvc = context.RequestServices.GetService(typeof(UserService)) as UserService;
// parse request body for email address and password
// user is returned incorrectly here
var user = await _userSvc.GetByEmailAsync(email);
}
}
You should use a local variable instead of a field to store the user service, otherwise your code is not thread-safe: request A begins and sets the field, but before it gets to the last line, request B begins and overwrites the field. Now when A gets to the last line, it's seeing the value written by request B.
@tuespetre Shoot, good catch! I have a couple other functions in this middleware that are called by Invoke(), that's why I was using a field. I altered them to accept a UserService parameter and I'm passing the local variable now.
Thanks again!