1

Am using EF Core 3.1.7 and have implemented some global query filters to control user access to some data in a couple of tables. The query filters need the currently logged in user id (an int in my implementation of identity) which I am holding in a variable within the DBContext class.

I am accessing the HttpContext within the DBContext constructor, via an injected IHttpContextAccessor, but find that sometimes when the DBContext is constructed, via DI (controller => data access service => DBContext), that the claims list for the user is empty and I cannot read the user id - and hence the global query filter fails as the param value is 0.

I don't understand why the following fails sometimes to set CurrentUserId to the current user's id (am certainly logged in). I would have thought that the same IHttpContextAccessor was used all the time and would always contain the current user.

    public int CurrentUserId { get; set; }
    public MyDbContext(DbContextOptions<MyDbContext> options, IHttpContextAccessor httpContextAccessor
        ) : base(options)
    {
        if (httpContextAccessor.HttpContext != null)
        {
            CurrentUserId = 0;
            var claimValue = httpContextAccessor.HttpContext?.User.FindFirstValue(ClaimTypes.NameIdentifier);
            if (claimValue != null)
            {
                CurrentUserId = Convert.ToInt32(claimValue);
            }
        }
    }

I have broken things down to the simplest scenario to attempt to remove as much clutter as possible but still find that when created the claimValue is sometimes null.

The IHttpContextAccessor is configured in startup using:

services.AddHttpContextAccessor();

And the DBContext is configured like:

services.AddDbContext<MyDbContext>(options => options.UseSqlServer(connectionString));
3
  • 1
    The HttpContext refers to a single request's context. If you try to use it outside a request, it will be null. It will also be null if you try to retrieve it before the middleware that establishes the request context Commented Aug 27, 2020 at 8:45
  • 1
    From an architectural POV this is something you shouldn't do anyway. Data access is an entirely different layer than HTTP requests. Your context shouldn't be aware of the type of application it happens to be used in. Commented Aug 27, 2020 at 8:48
  • @GertArnold You say that and you do not propose him any solution. What would your solution be when you say that for apply the filter in a multi-tenant aplication? Pass the userId in each method through all the methods in the entire application? Commented Dec 10, 2021 at 20:44

1 Answer 1

4

It would appear that I have solved this issue thanks to a number of sources. The issue was with attempting to retrieve the user's Id whilst still in the constructor of the DBContext. The answer lies in moving the code that accesses the user's claims to outside of the constructor and allow the claims to be read when they are really needed (which is well after the controller in question has been constructed).

Now the constructor for the DBContext has been simplified to only inject the IHttpContextAccessor:

    public MyDbContext(DbContextOptions<MyDbContext> options, IHttpContextAccessor httpContextAccessor) : base(options)
    {
        _httpContextAccessor = httpContextAccessor;
    }

A field in the DBContext now has a get method that looks up and returns the user's Id from their claims (if available).

public int CurrentUserId
        {
            get
            {
                if (_httpContextAccessor.HttpContext != null)
                {
                    var claimValue = _httpContextAccessor.HttpContext?.User?.FindFirstValue(ClaimTypes.NameIdentifier);
                    if (claimValue != null)
                    {
                        return Convert.ToInt32(claimValue);
                    }
                    return 0;
                }
                else
                {
                    return 0;
                }
            }
            set => throw new NotImplementedException();
        }

The global query filters look like this:

builder.Entity<Person>().HasQueryFilter(x => x.CompanyId == null || x.Company.UserCompanies.Any(y => y.UserId == CurrentUserId));

This has the effect of looking up the user's id from the claims when it is actually needed (when the query filter is applied) and the user's id is now available in the claims.

The issue was discussed in this post https://github.com/aspnet/Mvc/issues/7474 but the implementation seemed a bit clunky as it wanted controllers modified - I think this is a simpler method but am always open to ideas.

Sign up to request clarification or add additional context in comments.

4 Comments

This solution won't work because your CurrentUserId will be cached by EF... I'm struggling with this issue and found a solution that requires to override IModelCacheKeyFactory implementation that distinguish cache key by UserId but I'm having problems on how to trigger that UserId update correctly so cache is refreshed...
Hi, thanks for your comment but I must point out that this is working well in production and the CurrentUserId is looked up from the request each time a query needs the query filter.
Yeah I've figured it out :) You were registering dbcontext with AddDbContext and I with AddDbContextPool that's why it's working for you :)
this truly is a great question, answer and supporting comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.