0

Background

I have a website written in ASP.NET Core v2.1.1.

I have a custom identity user class:

public class FooIdentityUser : IdentityUser<string>, IIdentityModel
{
    [MaxLength(50)]
    public string FirstName { get; set; }
    [MaxLength(50)]
    public string LastName { get; set; }

    public string FullName => $"{FirstName} {LastName}";

    public bool FooBool { get; set; }
}

and a custom identity role class:

public class FooIdentityRole : IdentityRole<string>
{

}

Which I then reference in the dbcontext:

public class FooIdentityDbContext : IdentityDbContext<FooIdentityUser,FooIdentityRole,string>
{
    public FooIdentityDbContext(DbContextOptions<FooIdentityDbContext> options)
        : base(options)
    {
    }
}

Requirement

My overall requirement is that I want to give system admin users the ability to view and eventually manage user data from within the admin area of the website.

Specifically:

  • I want to provide a list of users that are in a foo role
  • And / or I want to list all users that have FooBool set to true
  • And / or I want to query on email address, first name & last name
  • And / or carry out a sort

Question

Does anyone have any links to web pages where this has been done before or can you respond on how I can implement this feature? I have attempted a couple of approaches below.

Approaches / Research

From what I can see there are two approaches to doing this:

Approach 1

Because I want to list users specifically for a user role based in a view, I can see that user manager provides a method for this:

_userManager.GetUsersInRoleAsync(fooRoleName)

The issue I have with this is it returns an IList so whilst it will return all users with this role, if I want to query on FooBool and / or FirstName, LastName or Email Address, it will need to cycle through the list to filter these out which would be inefficient if there are 10s of thousands or 100s of thousands of users?

Ideally, this would return an IQueryable so it wouldn't hit the database until my where and order by had been applied but I can't find a way of doing this?

Approach 2

The other way may be to query the context directly through my generic repository.

public class GenericIdentityRepository<TModel> : IIdentityRepository<TModel> where TModel : class, IIdentityModel
{
    private readonly ILogger _logger;
    public FooIdentityDbContext Context { get; set; }
    private readonly DbSet<TModel> _dbSet;

    public GenericIdentityRepository(FooIdentityDbContext dbContext, ILogger<GenericIdentityRepository<TModel>> logger)
    {
        Context = dbContext;
        _logger = logger;
        _dbSet = Context.Set<TModel>();
    }

    public IQueryable<TModel> GetAll()
    {
        _logger.LogDebug("GetAll " + typeof(TModel));
        IQueryable<TModel> query = _dbSet;
        return query;
    }

    public IQueryable<TModel> GetAllNoTracking()
    {
        _logger.LogDebug("GetAllNotTracking " + typeof(TModel));
        IQueryable<TModel> query = GetAll().AsNoTracking();
        return query;
    }
}

I was looking to see if I could do something by creating custom classes for userrole and then using linq to give me an IQueryable?

public class FooIdentityUserRole : IdentityUserRole<string>
{
    public virtual FooIdentityUser User { get; set; }
    public virtual FooIdentityRole Role { get; set; }
}

And then somehow query the data to return an IQueryable but I'm struggling to produce the correct linq I need to do this.

Neil
  • 601
  • 4
  • 20
  • I would just use the `FooIdentityDbContext` directly like you did in Approach 2 but.. would not try to do any generic repository. You have specific requirements like filtering by `FooBol` so.. why make it generic?. – jpgrassi Jan 15 '19 at 12:55
  • @jpgrassi. I've configured identity as per the answer in the following link: https://stackoverflow.com/questions/51004516/net-core-2-1-identity-get-all-users-with-their-associated-roles. How would you query all users assigned to fooRoleName and where FooBool is true either using the method in the link's example or dbcontext directly? When I try and do a where on Users property of userManager the typeahead doesn't give me access to query the roles table. _userManager.Users.Include(u => u.UserRoles).ThenInclude(ur => ur.Role).Where(ur=>ur...) – Neil Jan 21 '19 at 15:50
  • It's hard to evaluate this.. can you create a small, reproducible project and put it on Github, so I can take a look? If you configured everything, you should be able to access it.. – jpgrassi Jan 22 '19 at 08:30
  • @jpgrassi I've invited you to collaborate on my GitHub repo. I've just added a UserList action result to home controller. i.e. https://:/home/userlist Also, I've remmed out the line where the typeahead gives me a list of ApplicationUser properties instead of role properties. – Neil Jan 23 '19 at 15:53
  • didn't get any invite. – jpgrassi Jan 24 '19 at 14:41
  • hehe no! Doesn't matter, I'll fork it and take a look over the weekend :) – jpgrassi Jan 25 '19 at 15:07
  • @jpgrassi Assume you have the same username on Git? Showing as Juan Pablo? Here's a link to the repo https://github.com/neilmulhy/AspNetCore21IdentitySample – Neil Jan 25 '19 at 15:18
  • You don't need to send me an invite. Just provide the repo link and I can fork it. The link above is broken. – jpgrassi Jan 25 '19 at 15:21
  • I have it as a private repo which is why I thought I'd add you. What's your Git username? – Neil Jan 25 '19 at 15:22
  • I see. Here's is it then: https://github.com/joaopgrassi – jpgrassi Jan 25 '19 at 15:28
  • Thank you! I've added you now so hopefully you can see / fork it! – Neil Jan 25 '19 at 15:29
  • I've pushed a new branch with a working solution. Take a look there and if it works, I can post it here so others can benefit as well. – jpgrassi Jan 28 '19 at 20:36
  • That's great thanks @jpgrassi. The only thing I would say is where there's a user that is applied to multiple roles they will be listed multiple times in the view if there isn't a roleName parameter passed. The obvious solution I could see here would be to check if a query on the role is being passed and then look to the users db set instead of starting with the userroles, unless you have a better suggestion? – Neil Jan 29 '19 at 12:36
  • Right. There are multiple solutions to that, depends on your requirements. You could do what you said and it's fine as well, I think. In the end, we do `.Select(ur => ur.User)` so the UserRoles is used just to filter the role. Another approach would be to use `GroupBy` on `UserId` to avoid bringing duplicates.. but your view then does not display the roles the user is in. I'd say, figure out what you need to list first, and then work the query out. With my examples, I guess you can figure it out. – jpgrassi Jan 29 '19 at 12:44
  • Please put your solution as the answer! I'm sure it'll be helpful to others too! – Neil Jan 29 '19 at 13:45

2 Answers2

1

My suggestion is to use the FooIdentityDbContext directly in your controllers and just query the data in the way you want. I don't know a way you could achieve what you want using the UserManager class. Maybe there is but honestly, I wouldn't mix things. UserManager is more useful when you are dealing with a single user and want to do things with it such as AddToRoleAsync or ChangePasswordAsync.

You have much more flexibility using the DbContextclass directly. You don't need some fancy generic repository. Keep it simple and concise unless you definitely need the abstraction (which almost always you don't)

Down to the actual answer: You've already configured the entities correctly, so now just inject the FooIdentityDbContext and start querying. Something like this:

public class HomeController : Controller
{
    private readonly FooIdentityDbContext_dbContext;

    public HomeController(FooIdentityDbContext dbContext)
    {
        _dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext));
    }

    public async Task<IActionResult> UserList(string roleName, bool fooBool, string firstName)
    {
        // You are interested in Users on Roles, so it's easier to start on the UserRoles table
        var usersInRole = _dbContext.UserRoles.Select(userRole => userRole);

        // filter only users on role first
        if (!string.IsNullOrWhiteSpace(roleName))
        {
            usersInRole = usersInRole.Where(ur => ur.Role.Name == roleName);
        }

        // then filter by fooBool
        usersInRole = usersInRole.Where(ur => ur.User.FooBool == fooBool);

        // then filter by user firstname or whatever field you need
        if (!string.IsNullOrWhiteSpace(firstName))
        {
            usersInRole = usersInRole.Where(ur => ur.User.FirstName.StartsWith(firstName));
        }

        // finally materialize the query, sorting by FirstName ascending
        // It's a common good practice to not return your entities and select only what's necessary for the view.
        var filteredUsers = await usersInRole.Select(ur => new UserListViewModel
        {
            Id = ur.UserId,
            Email = ur.User.Email,
            FooBool = ur.User.FooBool,
            FirstName = ur.User.FirstName
        }).OrderBy(u => u.FirstName).ToListAsync();

        return View("UserListNew", filteredUsers);
    }
}

Bonus: I've been reading the EF Core in Action book by Jon Smith and it's great. I highly recommend reading it if you want to keep using EF Core in your projects. It's full of nice tips and real world examples.

jpgrassi
  • 5,482
  • 2
  • 36
  • 55
0

use .Users.

await _userManager.Users.Where(w => w.LastChangeUserId == null).ToListAsync();
M Komaei
  • 7,006
  • 2
  • 28
  • 34