AddScoped service is not retaining settings throughout the request - c#

I have created an Interface
public interface ICurrentUser
{
Task<bool> Set(UserAuth user);
User Get();
}
and a class
public class CurrentUserSvc : Interface.ICurrentUser
{
private User _u;
private UserAuth _ua;
private AppDbContext db;
public CurrentUserSvc(AppDbContext db) {
this.db = db;
}
public User Get()
{
return _u;
}
public async Task<bool> Set(UserAuth ua)
{
_ua = ua; // this is the default EntityFramework IdentityUser
_u = await db.AppUsers // this is my applicaiton's 'extra settings'
// user used to ensure passowrd fields are
// not passed about everywhere
.Where(u => u.UserID == _ua.UserID)
.SingleAsync();
return true;
}
}
In Startup.cs I set
services.AddScoped<ICurrentUser, CurrentUserSvc>();
// I also add a service which will be used later in a scoped
// lifecycle (though I've also tried transient on that one)
services.AddScoped<IProductDbSvc, ProductDbSvc>();
Later I call to a piece of middleware:
public async Task<Task> Invoke(HttpContext hc)
{
if (hc.User.Identity.IsAuthenticated) {
UserAuth iu = await _um.FindByIdAsync(hc.User.GetUserId());
await _cus.Set(iu);
}
// the values are definitely set correctly here.
// I have inspected them during debug
return _next(hc);
}
Later still I try to access the content of the CurrentUserSvc I try to access the current user via the GET
public ProductDbSvc(AppDbContext db, ICurrentUser cu){
this.db = db;
this.cu = cu;
// the values in cu are NULL here. Get() returns null
this.CurrentUser = cu.Get();
}
but the result of Get() is null I was expecting that a Scoped param would retain the values set earlier in the request lifecycle.
What am I missing? Is there some other way to ensure the scoped-singleton retains the user data throughout the application's lifecycle.
UPDATE: I've created a generic project that illustrates this problem generically. https://github.com/AlexChesser/AspnetIdentitySample
check out the repo
build and run in visualstudio or DNX
register a local user
try to view the service on http://localhost:5000/api/currentuser
You'll notice that within the DEBUG output you can see that the correct user details are set, but within the actual controller itself the values returned are null.
UPDATE 2 the working sample is on this branch in github https://github.com/AlexChesser/AspnetIdentitySample/tree/dependencyinjectionscoped
UPDATE 3 turns out scoped parameters can be injected into the INVOKE method of custom middleware as well. https://github.com/AlexChesser/AspnetIdentitySample/commit/25b010a5ae45678c137b2ad05c53ccd659a29101 altering the invoke method will allow for scoped parameters to be injected correctly.
public async Task Invoke(HttpContext httpContext,
ICurrentUserService cus,
UserManager<ApplicationUser> um)
{
if (httpContext.User.Identity.IsAuthenticated)
{
ApplicationUser au = await um.FindByIdAsync(httpContext.User.GetUserId());
await cus.Set(au);
}
await _next(httpContext);
}
UPDATE 4 - I discovered an issue with my middleware signature last night which is pretty important. Code above has been edited to the correct form. Specifically the method was Task<Task> and return _next(...)
This was resulting in a "whitescreen" death on certain page loads (async called badly will not throw a stack trace)
By altering to a Task and using await next(...) the code functions properly and eliminates the intermittent whitescreen death caused by badly implemented async in dotnet5.

DbContext is a scoped service and as well as your CurrentUserSvc is a scoped service. Middlewares are instantiated only once for the whole running time of the app, so they are singleton essentially. So you need to remove both DbContext and CurrentUserSvc from being constructor injected here.
Instead you can use HttpContext's RequestServices property (which returns a IServiceProvider) to resolve both the DbContext and CurrentUserSvc services.

In the middleware, inject a dependency to IServiceProvider, rather than ICurrentUser. Then in the Invoke get the current user via serviceProvider.GetRequiredService<ICurrentUser>();

Related

Session.SetString method throws exception "IFeatureCollection has been disposed. Object name: 'Collection'. " in ASP.NET Core 3.1

I have a project written in ASP.NET Core 3.1.
I need to set data to Session in Singleton service:
_session.SetString("some key", "some value");
I injected the session object from DI:
public OperatorService(ILogger<OperatorService> logger,
ISession session,
IOptions<AppSettings> options)
{
this._session = session;
this._logger = logger;
this._appSettings = options.Value;
}
I calls the my method as below:
public void ChangeOperatorStatus(StatusChangeRequest request)
{
try
{
_session.SetString(request.Key, request.Value);
}
catch (Exception ex)
{
_logger.LogInformation($"Exception while changing status: {ex}");
}
}
but I get the exception below :
IFeatureCollection has been disposed.\r\nObject name: 'Collection'.
and I added some code to Startup.cs's ConfigureServices method:
services.AddHttpContextAccessor();
services.AddSession(options =>
{
options.IdleTimeout = TimeSpan.FromMinutes(20);
options.Cookie.HttpOnly = true;
})
.AddDistributedMemoryCache();
And I added app.UseSession(); to the Configure method of Startup.cs.
I trid services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>(); and I get the session from httpContextAccessor.HttpContext.Session but I get the same error.
Please help me, thank you.
An HttpContext is the context of a single request. It provides access to the request, response properties etc of that single request. You can't cache it, it becomes invalid once that request ends.
Session is another transient thing - it lives only as long as a single user session. There's at least one session for every user of a web app. Caching one of those sessions in a singleton guarantees that
The reference will become invalid after a while, when the session expires and
The singleton will use only that user's values, ignoring everyone else's. This is a bug in itself, and a great way to hack into an application.
If an administrator logs in, the Session object may apply the admin's settings alive to everyone for the next 20, 30 or 60 minutes.
That's why using a Session makes sense for per-request middleware, not Singleton services.
Correct usage of HttpContext
The Session can only be reached through the request's context, so getting the correct session means getting the correct HttpContext. The correct way to do this is explained in David Fowler's ASP.NET Core Guidance :
❌ BAD This example stores the HttpContext in a field then attempts to use it later.
private readonly HttpContext _context;
public MyType(IHttpContextAccessor accessor)
{
_context = accessor.HttpContext;
}
public void CheckAdmin()
{
if (!_context.User.IsInRole("admin"))
{
throw new UnauthorizedAccessException("The current user isn't an admin");
}
}
✅ GOOD This example stores the IHttpContextAccesor itself in a field and uses the HttpContext field at the correct time (checking for null).
private readonly IHttpContextAccessor _accessor;
public MyType(IHttpContextAccessor accessor)
{
_accessor = accessor;
}
public void CheckAdmin()
{
var context = _accessor.HttpContext;
if (context != null && !context.User.IsInRole("admin"))
{
throw new UnauthorizedAccessException("The current user isn't an admin");
}
}
Use a Scoped service instead
Since a Singleton can't know what session to use. One option is to simply convert that service to a Scoped service. In ASP.NET Core, a request defines a scope. That's how controller actions and pipeline middleware get access to the correct HttpContext for each request.
Assuming the service is used by an action or middleware, perhaps the only change needed is to replace AddSingleton<ThatService> with AddScoped<ThatService>
Turning the tables, or Inversion of Control
Another option is for callers of that singleton should provide the session to it. Instead of using a cached session eg :
public void SetStatus(string status)
{
_session.SetString(SessionKeys.UserStatus, "some value");
}
Ask for the session or HttpContext as a parameter :
public void SetStatus(string status,ISession session)
{
session.SetString(SessionKeys.UserStatus, "some value");
}
And have callers pass the correct session to it
It took me a while to get this fixed.
In my case, it was a 3.1 aspnetcore and it didn't worked until I turn the container function from
public async void OnPost
to
public async Task<IActionResult> OnPost
Looks like the HttpContext was disposed before it was used...

Need async entrypoint for custom configuration of DbContext

Net Core and EF core does not support AAD tokens out of the box like full framework. There are a workaroudn were you can set access token on the SqlConnection. Retrieving the token is a async operation. So I need a generic entrypoint that are async. In constructor of my DbContext I can inject and execute stuff, but I cant do it async so it not good enough.
Any ideas? Thanks
internal class DbTokenConfig : IDbContextConfig
{
private readonly ITokenProvider _tokenProvider;
public DbTokenConfig(ITokenProvider tokenProvider)
{
_tokenProvider = tokenProvider;
}
public async Task Config(MyDbContext context)
{
var conn = context.Database.GetDbConnection() as SqlConnection;
conn.AccessToken = await _tokenProvider.GetAsync();
}
}
I need a async entrypoint were I can execute it, generic offcourse so any service that inject a DbContext will get it applied
edit: So basicly when doing
public class MyCommandHandler : ICommandHandler<MyCommand>
{
private readonly DbContext _ctx;
public MyCommandHandler(DbContext ctx)
{
_ctx = ctx;
}
public async Task Handle(MyCommand cmd)
{
await _ctx.Set<Foo>().ToListAsync(); //I want my access token to be applied before it opens connection
}
}
edit: Working solution
.AddDbContext<MyDbContext>(b => b.UseSqlServer(Configuration.GetConnectionString("MyDb")))
.AddScoped<DbContext>(p =>
{
var ctx = new AuthenticationContext("https://login.microsoftonline.com/xxx");
var result = ctx.AcquireTokenAsync("https://database.windows.net/", new ClientCredential("xxx", "xxx"))
.ConfigureAwait(false)
.GetAwaiter()
.GetResult();
var db = p.GetService<MyDbContext>();
((SqlConnection)db.Database.GetDbConnection()).AccessToken = result.AccessToken;
return db;
})
Just need to make the keys configurable, create a abstraction etc
There's a Github issue about this, so this is definitely not unclear. The issue is closed because there's no built-in support currently, a different issue tracks this.
The original issue describes a clever workaround though. First of all, UseSqlBuilder has an overload that accepts an existing DbConnection. This connection can be configured with an AAD token. If it's closed, EF will open and close it as needed. One could write :
services.AddDbContext<MyDBContext>(options => {
SqlConnection conn = new SqlConnection(Configuration["ConnectionString"]);
conn.AccessToken = (new AzureServiceTokenProvider()).GetAccessTokenAsync("https://database.windows.net/")
.Result;
options.UseSqlServer(conn);
});
The tricky part is how to dispose that connection.
The clever solution posted by Brian Ball is to implement an interface on the DbContext, and register that as the service that's used by controllers with a factory function. The DbContext still gets registered using its concrete type. The factory function gets that context and sets the AAD token to its connection :
services.AddDbContext<MyDbContext>(builder => builder.UseSqlServer(connectionString));
services.AddScoped<IMyDbContext>(serviceProvider => {
//Get the configured context
var dbContext = serviceProvider.GetRequiredService<MyDbContext>();
//And set the AAD token to its connection
var connection = dbContext.Database.GetDbConnection() as System.Data.SqlClient.SqlConnection;
if(connection == null) {/*either return dbContext or throw exception, depending on your requirements*/}
connection.AccessToken = //code used to acquire an access token;
return dbContext;
});
This way, the context's lifetime is still managed by EF Core. AddScoped<IMyDbContext> acts as a filter that takes that context and sets the AAD token
Next problem is how to write that //code used to acquire an access token; so it doesn't block.
This isn't so much of a problem because, according to the docs :
The AzureServiceTokenProvider class caches the token in memory and retrieves it from Azure AD just before expiration.
This code could be extracted into a factory method, and even get injected as a dependency.
Moving the goal posts
The main problem is that constructors can't be asynchronous yet so constructor injection can't retrieve tokens asynchronously.
What can be done though, is to register an asynchronous Func<> factory or service that's called in a controller's asynchronous actions instead of the constructor. Let's say :
//Let's inject configuration too
//Defaults stolen from AzureServiceTokenProvider's source
public class TokenConfig
{
public string ConnectionString {get;set;};
public string AzureAdInstance {get;set;} = "https://login.microsoftonline.com/";
public string TennantId{get;set;}
public string Resource {get;set;}
}
class DbContextWithAddProvider
{
readonly AzureServiceTokenProvider _provider;
readonly TokenConfig _config;
readonly IServiceProvider _svcProvider;
public DbContextWithAddProvider(IServiceProvider svcProvider, IOption<TokenConfig> config)
{
_config=config;
_provider=new AzureServiceTokenProvider(config.ConnectionString,config.AzureAdInstance);
_svcProvider=svcProvider;
}
public async Task<T> GetContextAsync<T>() where T:DbContext
{
var token=await _provider.GetAccessTokenAsync(_config.Resource,_config.TennantId);
var dbContext = _svcProvider.GetRequiredService<T>();
var connection = dbContext.Database.GetDbConnection() as System.Data.SqlClient.SqlConnection;
connection.AccessToken = token;
return dbContext;
}
}
This service should be registered as a singleton as it doesn't keep any state except the cached token, which we do want to keep around.
This can now be injected in a constructor, and called in an async action :
class MyController:Controller
{
DbContextWithAddProvider _ctxProvider;
public MyController(DbContextWithAddProvider ctxProvider)
{
_ctxProvider=ctxProvider;
}
public async Task<IActionResult> Get()
{
var dbCtx=await _ctxProvider.GetContextAsync<MyDbContext>();
...
}
}
I went through a similar though process almost 2 years ago where, in my last job, we decided to implement dynamic refreshing of the credentials for a DbContext object which it retrieved from Key Vault on the applications initial startup and then cached the credentials, if a connection failed then it was assumed that the credentials had changed or expired and it would retrieve them again and refresh the SqlConnection object (happy-path scenario, obviously there are other reasons for a connection to fail).
The problem then, and in this case, is that IServiceCollection has no asynchronous method available which allow you to invoke asynchronous delegates, so you have to use .Result when registering a service with asynchronous logic as a prerequisite.
What you could do is create a SqlConnection object with your access token and pass that to SqlServerDbContextOptionsExtensions.UseSqlServer within the AddDbContext<T> service registration in ConfigureServices. This ensures that every DbContext which is created will have an access token assigned, and with it being scoped by default it will have a new token per request.
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
services.AddScoped<ITokenProvider, TokenProvider>();
services.AddScoped<ISqlConnectionProvider, SqlConnectionProvider>();
services.AddDbContext<TestDbContext>((provider, options) =>
{
var connectionTokenProvider = provider.GetService<ITokenProvider>();
var sqlConnectionProvider = provider.GetService<ISqlConnectionProvider>();
var accessToken = connectionTokenProvider.GetAsync().Result; // Yes, I consider this to be less than elegant, but marking this delegate as async & awaiting would result in a race condition.
var sqlConnection = sqlConnectionProvider.CreateSqlConnection(accessToken);
options.UseSqlServer(sqlConnection);
});
}
The interface for ISqlConnectionProvider is
internal interface ISqlConnectionProvider
{
SqlConnection CreateSqlConnection(string accessToken);
}
In the implementation of ISqlConnectionProvider you'd have to
Inject an IOptions<T> object which contains the connection string details
Build or assign the connection string
Assign the access token
Return the SqlConnection object

EF Core multiple HTTP requests throws an error

I cannot seem to find an answer to this question.
So in the frontend when the user loads a page we call an API for each item on that page (10 items). So that equals 10 API calls.
Most of the calls work but there are always a few that fail when trying to query the database resulting in the following error:
InvalidOperationException: A second operation started on this
context before a previous operation completed. Any instance members
are not guaranteed to be thread safe.
Now I understand that Entity Framework is not thread safe but I am unsure how to get around this error.
Everywhere where I am using a DBContext it is always injected in using the built in .net core Ioc container.
Here is the DI setup
services.AddScoped<IOmbiContext, OmbiContext>();
services.AddTransient<ISettingsRepository, SettingsJsonRepository>();
All of my repositories are setup in a Transient scope with the Context as Scoped according to this article: https://learn.microsoft.com/en-us/aspnet/core/data/entity-framework-6
Now I have tried changing the context to Transient and it still happens.
How can I avoid this?
More Information
The API Method:
[HttpGet("movie/info/{theMovieDbId}")]
public async Task<SearchMovieViewModel> GetExtraMovieInfo(int theMovieDbId)
{
return await MovieEngine.LookupImdbInformation(theMovieDbId);
}
Which eventually calls the following where the exception is being thrown:
public async Task<RuleResult> Execute(SearchViewModel obj)
{
var item = await PlexContentRepository.Get(obj.CustomId); <-- Here
if (item != null)
{
obj.Available = true;
obj.PlexUrl = item.Url;
obj.Quality = item.Quality;
}
return Success();
}
PlexContentRepository
public PlexContentRepository(IOmbiContext db)
{
Db = db;
}
private IOmbiContext Db { get; }
public async Task<PlexContent> Get(string providerId)
{
return await Db.PlexContent.FirstOrDefaultAsync(x => x.ProviderId == providerId); <-- Here
}
If you use Entity Framework Core usually you do not need to add your Database Context as an additional service
I recommend to setup your DbContext in the Startup.cs as following:
services.AddEntityFrameworkSqlServer()
.AddDbContext<OmbiContext>();
Followed by a Controller class for your API calls taking the DBContext as constructor parameter.
public class ApiController : Controller
{
protected OmbiContext ctx;
public ApiController(OmbiContext dbctx)
{
ctx = dbctx;
}
public async Task<IActionResult> yourAsyncAction()
{
// access ctx here
}
}

'Cannot access a disposed object' error on UserManager.ResetPasswordAsync

I'm getting the following error when executing userManager.ResetPasswordAsync:
An unhandled exception occurred while processing the request.
ObjectDisposedException: Cannot access a disposed object.
Object name: 'TestDb'.
Microsoft.Data.Entity.DbContext.get_ServiceProvider()
I simplified the code so that it's easier to read. I'm calling the userManager twice in the controller lifetime. Once for generating the token and once for resetting the password:
private readonly UserManager<ApplicationUser> userManager;
// controller's constructor
public AuthController(UserManager<ApplicationUser> userManager) {
this.userManager = userManager;
}
[AllowAnonymous, HttpPost]
public async Task<ActionResult> ForgotPass(ForgotPassViewModel model) {
//model checks
var user = new UserQuery(db).GetUserByUserName(model.UserName);
//check if user exists
var token = await userManager.GeneratePasswordResetTokenAsync(user);
var url = $"{config.Url}/auth/resetpass?user={user.Id}&token={WebUtility.UrlEncode(token)}";
// send email with the reset url
model.Success = "An email has been sent to your email address";
return View(model);
}
[AllowAnonymous, HttpPost]
public async Task<ActionResult> ResetPass(ResetPassViewModel model) {
//model checks
var user = new UserQuery(db).GetUserById(model.UserId);
//error occurs here:
var result = await userManager.ResetPasswordAsync(user, model.Token, model.Password);
//check result
model.Success = "Password successfully reset";
return View(model);
}
Later Edit:
Here's a function from the UserQuery (as requested in comments below). I am indeed using the 'using' wrapper:
public ApplicationUser GetUserByUserName(string userName) {
using (var db = this.dbContext) {
var user = (from u in db.Users
where u.UserName == userName
select u).SingleOrDefault();
return user;
}
}
The using construct is a syntactic sugar around a
DbContext context = null;
try
{
context = new DbContext();
...stuff inside the using block ...
}
finally
{
if(context!=null)
context.Dispose()
}
It's same as calling
using(DbContext context = new DbContext())
{
...stuff inside the using block ...
}
block. This makes sure that the object is disposed as soon as possible and even when an exception happens (finally block is always called).
The DbContext in ASP.NET Core (specifically the ASP.NET Core Identity registration of it) is registered as with scoped life time, this means that the same reference will be returned each for the duration of the one request.
But when you prematurely dispose it (either with using block or by calling .Dispose() method yourself) before the request ends, it blows up when another method tries to access it.
The scoped life time is the recommended one, as the DbContext can use considerable amount of memory when it is very long living, because DbContext tracks changes of all records until you dispose it.
So in traditional applications without dependency injection or simple tutorials you create it with new and dispose it as soon as possible. But in an Web Application a request is pretty short-lived and scoped life-time keeps handle for most of the cases. There may be some corner cases where transient (AddTransient method in ASP.NET Core IoC container) lifetime is better.
If you really need transient resolution you could create a factory method and inject it to your services, something like:
services.AddTransient<Func<MyDbContext>>( (provider) => new Func<MyDbContext>( () => new MyDbContext()));
and inject it in your services/controller:
public class MyService
{
public readonly Func<MyDbContext> createMyContext;
public MyService(Func<MyDbContext> contextFactory)
{
this.createContext = contextFactory;
}
public User GetUserById(Guid userId)
{
// note we're calling the delegate here which
// creates a new instance every time
using(var context = createContext())
{
return context.User.FirstOrDefault(u => u.Id = userId);
}
}
}
This won't cause the issue, but is more complicated than necessary. And if you need transactions this may not play well as the transactions are per DbContext instance and Identity will always use the scoped one
It seems the method userManager.ResetPasswordAsync() used some delay load property of the user variable. Since the user variable is out of the DB query scope, so that the property is not accessible.
I replaced my custom User queries with the built in userManager queries that do the same thing and it works now:
In ForgotPass function:
var user = await userManager.FindByEmailAsync(model.UserName);
In ResetPass function:
var user = await userManager.FindByIdAsync(model.UserId);
I'll update the answer once I know exactly why my initial approach didn't work.

Accessing DbContext in Middleware in ASP.NET 5

I wrote my custom middleware which I add in
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
//...
app.UseAutologin();
app.UseMvc(routes =>
{
//...
So it is the last middleware before the Mvc comes into play.
In my middleware's Invoke method I want to (indirectly) access the DbContext.
public async Task Invoke(HttpContext context)
{
if (string.IsNullOrEmpty(context.User.Identity.Name))
{
var applicationContext = _serviceProvider.GetService<ApplicationDbContext>();
var signInManager = _serviceProvider.GetService<SignInManager<ApplicationUser>>();
var result = await signInManager.PasswordSignInAsync(_options.UserName, _options.Password, true, false);
}
await _next(context);
}
Nearly every time I get the following exception:
InvalidOperationException: An attempt was made to use the context
while it is being configured. A DbContext instance cannot be used
inside OnConfiguring since it is still being configured at this point.
Now this is clearly raised by the PasswordSignInAsync method. But how can I ensure that the model was created before doing such things?
Maybe I was not entirely clear: I don't want to use the DbContext myself - the PasswordSignInAsync uses it when verifying the user and password.
What if you inject the ApplicationDbContext and SignInManager<ApplicationUser> through the Invoke method:
public async Task Invoke(HttpContext context, ApplicationDbContext applicationContext, SignInManager<ApplicationUser> signInManager)
{
if (string.IsNullOrEmpty(context.User.Identity.Name))
{
var result = await signInManager.PasswordSignInAsync(_options.UserName, _options.Password, true, false);
}
await _next(context);
}
This way you the services are resolved from the correct scope. I notice you don't actually use the ApplicationDbContext anywhere, just the SignInManager. Do you really need it?
This error is likely occurring because any middleware acts as a singleton. You have to avoid using member variables in your middleware. Feel free to inject into the Task Invoke, but don't store the inject value into a member object.
See: Saving HttpContext Instance in Middleware,
Calling services in Middleware
I was able to get around this myself, by creating a class that I could then pass into other methods in my middleware:
public async Task Invoke(HttpContext context, IMetaService metaService)
{
var middler = new Middler
{
Context = context,
MetaService = metaService
};
DoSomething(middler);
}
Just by this:-
public async Task Invoke(HttpContext context)
{
var dbContext = context.RequestServices.GetRequiredService<ClinicDbContext>();
await _next(context);
}
This is the simple solution that works great for my use case.
I created a simple method I can call from anywhere in the application to easily get the database context:
public class UtilsApp
{
public static MyDbContext GetDbContext()
{
DbContextOptionsBuilder<MyDbContext> opts =
new DbContextOptionsBuilder<MyDbContext();
optionsBuilder.UseSqlServer(Program.MyDbConnectionString); // see connection string below
return new MyDbContext(opts.Options);
}
}
Then, to use it anywhere in the application:
MyDbContext dbContext = UtilsApp.GetDbContext();
I set Program.MyDbConnectionString (a public static string field) from within Startup.ConfigureServices() (which is a callback that is called from within Program.Main() via CreateHostBuilder(args).Build()). That way I can use that connection string anywhere in the application without having to repeatedly retrieve it from appsettings.json or an environment variable.

Categories

Resources