Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

.net8.0 - NullReferenceException while using the UserManager.Users #791

Open
ahmadqarshi opened this issue Feb 26, 2024 · 4 comments
Open

Comments

@ahmadqarshi
Copy link

I created a brand new .net8.0 API project in Visual Studio 2022.
Added the .Net Core Identity support with custom User MyAppUser and Role MyAppRole classes.

I added the DbContext class like below

public class SchoolContext : IdentityDbContext<MyAppUser, MyAppRole, Guid>
{
    private readonly IHttpContextAccessor _httpContextAccessor;

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

Inside a sample AuthController, I added an Authenticate action. Sample code snippet is given below

public class AuthController : ControllerBase
{
    private readonly UserManager<MyAppUser> _userManager;

    public AuthController(UserManager<MyAppUser> userManager)
    {
        _userManager = userManager;
    }

    [HttpPost("Login")]
    public async Task<ApiResponse> Authenticate([FromBody] LoginUserRequest model)
    {
        try
        {
            var user = _userManager.Users.FirstOrDefault(u => u.Email == model.Email);
            if (user == null)
            {
               throw new ApiException("Invalid Credentials", 400);
            }
            .............................
        }
        catch (Exception ex)
        {
                .......
        }
    }
}

Upto this point calling the Authenticate action works fine.

Now, I installed the Finbuckle.MultiTenant.AspNetCore and Finbuckle.MultiTenant.EntityFrameworkCore packages.

In the Program.cs added below lines of code

builder.Services.AddMultiTenant<TenantInfo>()
    .WithPerTenantAuthentication()
    .WithHostStrategy()
    .WithConfigurationStore();

and at the end

........
app.UseMultiTenant();
.........
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();

Also added the [MultiTenant] attribute to the MyAppUser and MyAppRole classes. Finally, inherited the DbContext class from MultiTenantIdentityDbContext. Created and applied the migration and ran the application.

Invoking the same Authenticate action, throws NullReferenceException at var user = _userManager.Users.FirstOrDefault(u => u.Email == model.Email);

image

Stack trace is given below:

   at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.Execute[TResult](Expression query)
   at Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryProvider.Execute[TResult](Expression expression)
   at Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryable`1.GetEnumerator()
   at Microsoft.EntityFrameworkCore.Internal.InternalDbSet`1.System.Collections.Generic.IEnumerable<TEntity>.GetEnumerator()
   at System.Collections.Generic.LargeArrayBuilder`1.AddRange(IEnumerable`1 items)
   at System.Collections.Generic.EnumerableHelpers.ToArray[T](IEnumerable`1 source)
   at System.Linq.SystemCore_EnumerableDebugView`1.get_Items()

The problem persists even if the custom User and Role classes are not used.

See the sample github repo here

@AndrewTriesToCode
Copy link
Sponsor Contributor

AndrewTriesToCode commented Feb 26, 2024

Hi, thanks for providing all the detail. When you hit the auth controller is there a resolved tenant at the time?

@ahmadqarshi
Copy link
Author

Hi Andrew, thanks for your prompt response.
At the moment tenant is not resolved. However, if I add a subdomain like https://tenant-1.localhost:7046/api/auth/Login, it gets resolved and the UserManager works as per the expectations.

There are going to be two cases when we will not have the tenant information.

  1. A new tenant submits a signup request.
  2. We are going to have a single front-end application. So we assume that when the user submits the login request, after successful authentication, on the client side, we will get the tenant information from the API, and in the subsequent requests, we will submit the tenant identifier (most probably in the __tenant__ header after changing the strategy to WithHeaderStrategy).

For the signup, after saving the tenant information, I tried setting the TenantInfo using the TrySetTenantInfo function but the call to the _userManager.Users.FirstOrDefault(....) failed again. Sample code is shown below:

public AuthController(UserManager<MyAppUser> userManager, SchoolContext dbContext, IHttpContextAccessor httpContextAccessor)
{
    _userManager = userManager;
    _dbContext = dbContext;
    _httpContextAccessor = httpContextAccessor;
}

[HttpPost("Signup")]
public async Task<ApiResponse> Signup([FromBody] TenantSignupRequest model)
{
    var newTenantInfo = new TenantInfo
    {
        Id = Guid.NewGuid().ToString(),
        Identifier = model.Identifier,
        Name = model.Name
    };

    //Save the new Tenant
    //Save the AspNetUser record
    //Save the AspNetUserRole record

    if (_httpContextAccessor.HttpContext.TrySetTenantInfo(newTenantInfo, true))
    {
        var tenant = _httpContextAccessor.HttpContext.GetMultiTenantContext<TenantInfo>(); //TenantInfo is successfully fetched here.
    }

    try
    {
        var user = _userManager.Users.FirstOrDefault(u => u.Email == model.Email); //NullReferenceException is thrown here as well
        if (user != null)
        {
            var token = await _userManager.GeneratePasswordResetTokenAsync(user);
            var callbackUrl = QueryHelpers.AddQueryString("https://abc.com/some/callback/url","token", token);
            //Send callbackUrl in an email.
        }
    }
    catch (Exception ex)
    {

        throw;
    }
}

@AndrewTriesToCode
Copy link
Sponsor Contributor

hi, I need to create some convenience methods for getting a user manager or similar for a given tenant without having to rely on DI. Until then consider this:

HttpContext.TrySetTenantInfo(newTenantInfo, true);
// get the user manager from the new service provider scope, the one from DI was created with the original or no tenant:
var userManager = HttpContext.RequestService.GetService<UserManager<MyAppUser>>();

Once the DI scope is reset with TrySetTenantInfo you'll want to re-resolve the user manager so it takes the new tenant into account. You might even want to remove the constructor DI for it. Constructor DI is usually preferred but this is a unique situation where using the service provider (aka service locator pattern) is justified.

Does that work for you?

@ahmadqarshi
Copy link
Author

ahmadqarshi commented Feb 28, 2024

That's great @AndrewTriesToCode.

It works after re-resolving the UserManager.

However for the Login functionality, as we are using a single frontend application, there is no way for us to know the Tenant of the user unless they explicitly furnish the tenant ID alongside their credentials.

Is there any solution for this?

As a workaround, I am thinking of creating a separate DbContext that doesn't inherit from the MultiTenantIdentityDbContext. something like below

public class HostDbContext: IdentityDbContext<ApplicationIdentityUser, ApplicationRole, int>
{
....
}

and then in my Controller find the user's Tenant, set it, and then perform the operations something like below

public class AuthController : ControllerBase
{
  private readonly IHttpContextAccessor __httpContextAccessor;
  private UserManager<MyAppUser> _userManager;
  private HostDbContext _hostDbContext;
  private SchoolDbContext _dbContext;

  public AuthController(HostDbContext hostDbContext, IHttpContextAccessor httpContextAccessor)
  {
    _hostDbContext = hostDbContext;
    _httpContextAccessor = httpContextAccessor;
  }

  [HttpPost("Login")]
  public async Task<ApiResponse> Authenticate([FromBody] LoginUserRequest model)
  {
    try
    {
      var user =_hostDbContext.Users.FirstOrDefault(u => u.Email == email);
      if (user != null)
      {
        var tenant = _hostDbContext.Tenants.Find(user.TenantId);
        if(tenant != null)
        {
          if (_httpContextAccessor.HttpContext.TrySetTenantInfo<TenantInfo>(tenant, true))
          {
            _userManager = _httpContextAccessor.HttpContext.RequestServices.GetService<UserManager<MyAppUser>>();
            _dbContext = _httpContextAccessor.HttpContext.RequestServices.GetService<SchoolDbContext>();
            //Now we have tenant aware _userManager and _dbContext. Use them for the rest of the operations.
            return PerformAuthentication(model); 
          }
        }
      }            
    }
    catch (Exception ex)
    {
      .......
    }
  }
}

Is this right, or is there any better way?

I have to manage Tenants later down the line for support purposes and I am assuming that this separate DbContext will help me to do that (if there is no other way to allow a particular user to access the Tenant's Data).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Development

No branches or pull requests

2 participants