I am stuck and I can't seem to figure this out. I have a simple class with an interface. I am injecting EFContext and Logger into this service. For some reason, no matter how I register the service it's always a singleton. I put the Guid property on the class to see if it changed on each request, but it stays the same.
Here is the AccountService class and its interface:
public interface IAccountService
{
Account GetAccountByEmailAndPassword(string emailAddress, string password);
}
public class AccountService : IAccountService
{
private readonly IEFContext _context;
private readonly ILogger<AccountService> _logger;
private string _guid;
public AccountService()
{
_context = context;
_logger = logger;
_guid = Guid.NewGuid().ToString();
}
public Account GetAccountByEmailAndPassword(string emailAddress, string password)
{
try
{
//get the account
var account = _context.Account.FirstOrDefault(x => x.EmailAddress == emailAddress);
//make sure we have an account
if (account == null)
return null;
//generate hash from account
var accountHash = GeneratePasswordSaltHash(account.Password, account.PasswordSalt);
//generate hash from credentials passed in
var passedInHash = GeneratePasswordSaltHash(
Convert.ToBase64String(HashPassword(password)),
account.PasswordSalt);
// TODO: number of failed attempts should lock account etc.
return accountHash == passedInHash ? account : null;
} catch (Exception ex)
{
_logger.LogError("Exception in AccountService: " + ex.ToString());
throw;
}
}
}
Here is how I am registering the services:
public void ConfigureServices(IServiceCollection services)
{
// App Settings
services.Configure<AppSettings>(Configuration.GetSection("AppSettings"));
// Add DBContext
var connectionString = Configuration["AppSettings:Data:ConnectionString"];
services.AddDbContext<EFContext>(options => options.UseSqlServer(connectionString));
// Add framework services.
services.AddMvc();
// Add DI
services.AddScoped<IEFContext, EFContext>();
services.AddScoped<IAccountService, AccountService>();
}
Here is the EFContext class and its interface:
public interface IEFContext
{
DbSet<Account> Account { get; set; }
int SaveChanges();
EntityEntry Update(object entity);
}
public class EFContext : DbContext, IEFContext
{
public EFContext(DbContextOptions options) : base(options) {}
public DbSet<Account> Account { get; set; }
}
I can hit the database and all that with the context, but, everything is a singleton. I was first alerted to the issue because if I went into the database and manually updated some data on an Account, then requested the account again in code, the data would come back stale. I thought it was a context issue but I think I'm configuring the context lifecycle correctly by using .AddScoped<>
, but I couldn't get it to work. So then I tried adding the _guid
property to the AccountService
to determine if it was getting newd up on each request and it doesn't appear to be. I have tried .AddTransient<>
as well. Any help is appreciated. Thanks so much.
EDIT Here is my configure method:
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
loggerFactory.AddConsole(Configuration.GetSection("Logging"));
loggerFactory.AddDebug();
loggerFactory.AddSerilog();
//Token stuff
// secretKey contains a secret passphrase only your server knows
var secretKey = "mysupersecret_secretkey!123";
var signingKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(secretKey));
var tokenValidationParameters = new TokenValidationParameters
{
// The signing key must match!
ValidateIssuerSigningKey = true,
IssuerSigningKey = signingKey,
// Validate the JWT Issuer (iss) claim
ValidateIssuer = true,
ValidIssuer = "ExampleIssuer",
// Validate the JWT Audience (aud) claim
ValidateAudience = true,
ValidAudience = "ExampleAudience",
// Validate the token expiry
ValidateLifetime = true,
// If you want to allow a certain amount of clock drift, set that here:
ClockSkew = TimeSpan.Zero
};
app.UseJwtBearerAuthentication(new JwtBearerOptions
{
AutomaticAuthenticate = true,
AutomaticChallenge = true,
TokenValidationParameters = tokenValidationParameters
});
// Token generator
var options = new TokenProviderOptions
{
Audience = "ExampleAudience",
Issuer = "ExampleIssuer",
SigningCredentials = new SigningCredentials(signingKey, SecurityAlgorithms.HmacSha256),
};
app.UseMiddleware<TokenProviderMiddleware>(Options.Create(options));
app.UseMvc();
}
In my Token middleware, I do have it using the AccountService, here is the token middleware:
public class TokenProviderMiddleware
{
private readonly RequestDelegate _next;
private readonly TokenProviderOptions _options;
private readonly IAccountService _accountService;
public TokenProviderMiddleware(RequestDelegate next, IOptions<TokenProviderOptions> options, IAccountService accountService)
{
_next = next;
_options = options.Value;
_accountService = accountService;
}
public Task Invoke(HttpContext context)
{
// If the request path doesn't match, skip
if (!context.Request.Path.Equals(_options.Path, StringComparison.Ordinal))
{
return _next(context);
}
if (!context.Request.Method.Equals("POST")
|| !context.Request.ContentType.Contains("application/json"))
{
context.Response.StatusCode = 400;
return context.Response.WriteAsync("Bad request.");
}
return GenerateToken(context);
}
private async Task GenerateToken(HttpContext context)
{
var rawAccount = await new StreamReader(context.Request.Body).ReadToEndAsync();
var authAccount = JsonConvert.DeserializeObject<AuthAccount>(rawAccount);
var account = _accountService.GetAccountByEmailAndPassword(authAccount.EmailAddress, authAccount.Password);
if (account == null)
{
context.Response.StatusCode = 400;
await context.Response.WriteAsync("Invalid email address or password.");
return;
}
var now = DateTime.UtcNow;
// Specifically add the jti (random nonce), iat (issued timestamp), and sub (subject/user) claims.
// You can add other claims here, if you want:
var claims = new Claim[]
{
new Claim(JwtRegisteredClaimNames.Sub, account.EmailAddress),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
new Claim(JwtRegisteredClaimNames.Iat, ((DateTimeOffset)now).ToUnixTimeSeconds().ToString(), ClaimValueTypes.Integer64),
new Claim(ClaimTypes.Role, account.RoleId.ToString()),
new Claim(ClaimTypes.Name, account.EmailAddress)
};
// Create the JWT and write it to a string
var jwt = new JwtSecurityToken(
issuer: _options.Issuer,
audience: _options.Audience,
claims: claims,
notBefore: now,
expires: now.Add(_options.Expiration),
signingCredentials: _options.SigningCredentials);
var encodedJwt = new JwtSecurityTokenHandler().WriteToken(jwt);
var response = new ApiResponse<AuthAccount>
{
StatusCode = (int)HttpStatusCode.OK,
Message = "Access granted",
Data = new AuthAccount
{
Access_Token = encodedJwt,
Expires_In = (int)_options.Expiration.TotalSeconds
}
};
// Serialize and return the response
context.Response.ContentType = "application/json";
await context.Response.WriteAsync(JsonConvert.SerializeObject(response, new JsonSerializerSettings { Formatting = Formatting.Indented }));
}
}
Transient services that are stateless and don't contain any stateful dependencies can be injected into singleton or scoped services.
If a singleton service has a dependency on a transient service, the transient service may also require thread safety depending on how it's used by the singleton.
Scoped approach => This is a better option when you want to maintain a state within a request. Transient approach => Use this approach for the lightweight service with little or no state.
This example registers a DbContext subclass called ApplicationDbContext as a scoped service in the ASP.NET Core application service provider (a.k.a. the dependency injection container). The context is configured to use the SQL Server database provider and will read the connection string from ASP.NET Core configuration.
Middleware is only instantiated once, so it's a singleton effectively.
Everything you inject into the Middlewares constructor is hence resolved from the singleton container (the one you can access via app.ApplicationServices
inside Configure method).
I see that your IAccountService
is injected into the middleware, so that seems to cause the issue. You must resolve it on per context basis in Invoke method, using
public Task Invoke(HttpContext context, IAccountService accountService)
{
// If the request path doesn't match, skip
if (!context.Request.Path.Equals(_options.Path, StringComparison.Ordinal))
{
return _next(context);
}
if (!context.Request.Method.Equals("POST")
|| !context.Request.ContentType.Contains("application/json"))
{
context.Response.StatusCode = 400;
return context.Response.WriteAsync("Bad request.");
}
return GenerateToken(context, accountService);
}
or
public Task Invoke(HttpContext context)
{
var accountService = context.RequestServices.GetRequiredService<IAccountService>();
// If the request path doesn't match, skip
if (!context.Request.Path.Equals(_options.Path, StringComparison.Ordinal))
{
return _next(context);
}
if (!context.Request.Method.Equals("POST")
|| !context.Request.ContentType.Contains("application/json"))
{
context.Response.StatusCode = 400;
return context.Response.WriteAsync("Bad request.");
}
return GenerateToken(context, accountService);
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With