Creating Default Dashboards using Roles

Today, we demonstrate how to create default dashboards in Tuxboard based on a user's role

Written by Jonathan "JD" Danylko • Last Updated: • Tuxboard •

Two actors in a role at a bar

In the last post, we demonstrated how to create user-specific dashboards for any user signing in and providing standard widgets for every user.

However, what if you have different user permissions? If we include administrative widgets in a standard user's dashboard, we may have some problems down the road.

In today's post, we'll look at how to create default dashboards for different roles using Microsoft Identity. While this technique is focusing on Microsoft Identity, it can easily be modified to work with other security frameworks like Microsoft Entra.

The example we'll review is in the 10-Default-Dashboards folder. The 10-Default-Dashboards project has already gone through all of these steps. All that's required to run is the initial setup and migrations.

Getting Started

As always, there is a repository available of everything we'll cover in this post at Tuxboard.Examples.

At first, we'll setup the project so we can see how it runs and then follow along of how the project was built.

To get started with the finished project,

  1. Right-click on the package.json file and Restore Packages so the client-side JavaScript/TypeScript will work as expected.
  2. Open the appsettings.json file and update the connection string.
  3. Open the Package Manager Console (View > Other Windows... > Package Manager Console)
  4. Confirm you have the 10-Default-Dashboards project selected in the Default Project Dropdown in the Package Manager Console.
  5. Since we already have a migration in place, type update-database -Context TuxboardRoleDbContext

After updating the database, users and their roles are the only things missing to complete this demonstration.

If you'd like to fast-forward to creating the users and roles, jump down to the "Configuring the Database" section to successfully run the project.

With that said, we can now review the project to understand how to create default dashboards in our own projects.

Understanding Default Dashboards

Default dashboards are meant to give a specific dashboard and/or collection of widgets to different types of users. If a user logged into a website and were given a blank screen, it wouldn't be a very good experience for the user, would it?

Since we only have a single widget/default dashboard available, we may want to add a dashboard for new users with a tutorial widget or a Getting Started widget. If they're an administrator, they would want to see a statistics widget on how many users logged in for the day, what notifications happened since the last log in, or even a to-do list.

In the database, the tables to focus on are the DefaultDashboard and Layout tables.

The DefaultDashboard table provides a LayoutID and PlanID. Originally, the Tuxboard schema was meant for subscription plans (DefaultDashboard table) and widgets available based on a plan (WidgetPlan table). Since we're using roles with Identity, we won't be using the PlanID field in the DefaultDashboard table or the WidgetPlan table.

The Layout table is what's important. If there is a null in the TabID, it's a Default Dashboard layout of some kind. Every time a user logs in, they're given a TabID for their dashboard and a layout is assigned to the tab. Basically, an empty TabID means it doesn't belong to a user.

After running the update-database command, two default dashboard records are added and two layout records are added.

The last piece we need for this project is the Roles <-> DefaultDashboard relationship which we'll get into in a bit.

Adding Identity Models

Once we copied everything over from the user-specific dashboards project to create our new project, we're ready to use Microsoft Identity immediately.

Unfortunately, there is an issue with our schema: the DefaultDashboard table uses a GUID and the Roles table in the Identity schema uses a string by default so we need to adjust our Identity models to match what we need.

public class TuxboardUser : IdentityUser<Guid>
public class TuxboardUserClaim : IdentityUserClaim<Guid>
public class TuxboardUserLogin : IdentityUserLogin<Guid>
public class TuxboardUserRole : IdentityUserRole<Guid>
public class TuxboardUserToken : IdentityUserToken<Guid>
public class TuxboardUserStore : UserStore<TuxboardUser, TuxboardRole, TuxboardRoleDbContext, Guid,
    TuxboardUserClaim, TuxboardUserRole, TuxboardUserLogin, TuxboardUserToken, TuxboardRoleClaim>

public
 class TuxboardRole : IdentityRole<Guid> public class TuxboardRoleClaim : IdentityRoleClaim<Guid> public class TuxboardRoleStore : RoleStore<TuxboardRole, TuxboardRoleDbContext,      Guid, TuxboardUserRole, TuxboardRoleClaim>

These models were created based on the Identity post on learn.microsoft.com called Identity Model Customization. Based on the above class signatures, we need a TuxboardRoleDbContext.

public interface ITuxboardRoleDbContext : ITuxDbContext
{
    DbSet<RoleDefaultDashboard> RoleDefaultDashboards { get; set; }

    // Identity     DbSet<TuxboardUserClaim> TuxboardUserClaims { get; set; }     DbSet<TuxboardUserRole> TuxboardUserRoles { get; set; }     DbSet<TuxboardUserLogin> TuxboardUserLogins { get; set; }     DbSet<TuxboardUserToken> TuxboardUserTokens { get; set; }     DbSet<TuxboardUser> TuxboardUsers { get; set; }     DbSet<TuxboardRole> TuxboardRoles { get; set; }     DbSet<TuxboardRoleClaim> TuxboardRoleClaims { get; set; } }
public
 class TuxboardRoleDbContext : TuxDbContext, ITuxboardRoleDbContext {     public TuxboardRoleDbContext(DbContextOptions<TuxDbContext> options, IOptions<TuxboardConfig> config)         : base(options, config)     {     }
    public DbSet<RoleDefaultDashboard> RoleDefaultDashboards { get; set; }

   // Identity     public DbSet<TuxboardUserClaim> TuxboardUserClaims { get; set; }     public DbSet<TuxboardUserRole> TuxboardUserRoles { get; set; }     public DbSet<TuxboardUserToken> TuxboardUserTokens { get; set; }     public DbSet<TuxboardUserLogin> TuxboardUserLogins { get; set; }     public DbSet<TuxboardUser> TuxboardUsers { get; set; }     public DbSet<TuxboardRole> TuxboardRoles { get; set; }     public DbSet<TuxboardRoleClaim> TuxboardRoleClaims { get; set; }
    protected override void OnModelCreating(ModelBuilder modelBuilder)     {         base.OnModelCreating(modelBuilder);
        modelBuilder.ApplyConfiguration(new RoleDefaultDashboardConfiguration());         modelBuilder.ApplyConfiguration(new DashboardLayoutConfiguration());         modelBuilder.ApplyConfiguration(new DashboardLayoutRowConfiguration());         modelBuilder.ApplyConfiguration(new DashboardDefaultConfiguration());         modelBuilder.ApplyConfiguration(new DashboardDefaultWidgetConfiguration());
        // Identity         modelBuilder.ApplyConfiguration(new TuxboardRoleConfiguration());         modelBuilder.ApplyConfiguration(new TuxboardRoleClaimConfiguration());         modelBuilder.ApplyConfiguration(new TuxboardUserConfiguration());         modelBuilder.ApplyConfiguration(new TuxboardUserClaimConfiguration());         modelBuilder.ApplyConfiguration(new TuxboardUserLoginConfiguration());         modelBuilder.ApplyConfiguration(new TuxboardUserRoleConfiguration());         modelBuilder.ApplyConfiguration(new TuxboardUserTokenConfiguration());     } }

Even though the TuxboardRoleDbContext is small, there is a lot to unpack here along with some interesting insights.

Multiple DbContexts into One

In regards to Entity Framework DbContexts, it's recommended to minimize the number of DbContext's used in an application. As a matter of fact, if you can get away with one DbContext, it's for the best. It also makes it easier to track entity state changes.

Since we already have a TuxDbContext, how could we "attach" Identity models to the existing DbContext?

Simple. Add the DbSets and Configurations for each Identity models in the inherited DbContext.

One of the shockers (for me anyways) was the ability to attach Identity models to an existing DbContext and have it work as expected instead of having one DbContext for TuxDbContext and one for Identity.

Inheriting Aggregates Configurations

The configurations was another shock as to the power of Entity Framework Core.

For the standard Tuxboard configurations above, we are simply adding additional records to each table. While we already had Tuxboard configurations in the base TuxDbContext class, we're adding more configurations for additional Layout and LayoutRow records.

When we did a migration of the new TuxboardRoleDbContext, all configurations were combined into one making the records inserted as one batch.

For example, the DashboardLayoutConfiguration class above consists of the following code.

public class DashboardLayoutConfiguration: IEntityTypeConfiguration<Layout>
{
    public void Configure(EntityTypeBuilder<Layout> builder)
    {
        builder.HasData(new List<Layout>
        {
            new()
            {
                LayoutId = new Guid("239C89ED-3310-40D8-9104-237659415392"),
                TabId = null,
                LayoutIndex = 1
            }
        });
    }
}

However, when the migration finished, the code consisted of the following code.

migrationBuilder.InsertData(
    schema: "dbo",
    table: "Layout",
    columns: new[] { "LayoutId", "LayoutIndex", "TabId" },
    values: new object[,]
    {
        { new Guid("239c89ed-3310-40d8-9104-237659415392"), 1, null },
        { new Guid("5267da05-afe4-4753-9cee-d5d32c2b068e"), 1, null }
    });

It used the .HasData() method from both the TuxDbContext and TuxboardRoleDbContext

Creating the Junction/Association Table

While integrating Identity into the existing TuxDbContext was a happy experience, we need to focus on our target: associating a role to a default dashboard.

For those who were paying attention, there was a DbSet called RoleDefaultDashboards in our new TuxboardRoleDbContext. This is our junction table and the model is based on the following code.

public class RoleDefaultDashboard
{
    public virtual Guid DefaultDashboardId { get; set; }
    public virtual Guid RoleId { get; set; }

    public virtual DashboardDefault DefaultDashboard  { get; set; } = default!;     public virtual TuxboardRole Role { get; set; } = default!; }

The RoleDefaultDashboardConfiguration class consists of the code below.

public class RoleDefaultDashboardConfiguration: IEntityTypeConfiguration<RoleDefaultDashboard>
{
    public void Configure(EntityTypeBuilder<RoleDefaultDashboard> builder)
    {
        builder.HasKey(r => new { r.DefaultDashboardId, r.RoleId });
        builder.HasData(new List<RoleDefaultDashboard>
        {
            new()
            {
                RoleId = new Guid("7E69EB1F-07C0-46A1-B4E8-86F56386C250"), // Admin
                DefaultDashboardId = new Guid("0D96A18E-90B8-4A9F-9DF1-126653D68FE6") // Admin Dashboard
            },
            new()
            {
                RoleId = new Guid("31C3DF95-FDC6-4FB5-82AB-0436EA93C1B1"), // Basic
                DefaultDashboardId = new Guid("1623F469-D9F0-400C-8A4C-B4366233F485") // Basic dashboard
            }
        });
    }
}

One thing missing is how we load our dashboard based on a user's role...which brings us to our RoleDashboardService.

public interface IRoleDashboardService
{
    Task<DashboardDefault> GetDashboardTemplateByRoleAsync(TuxboardUser user);
    Task<bool> DashboardExistsForAsync(Guid userId);
}

public
 class RoleDashboardService : IRoleDashboardService {     private readonly ITuxboardRoleDbContext _context;     private readonly UserManager<TuxboardUser> _userManager;     private readonly RoleManager<TuxboardRole> _roleManager;
    public RoleDashboardService(ITuxboardRoleDbContext context,         UserManager<TuxboardUser> userManager,         RoleManager<TuxboardRole> roleManager)     {         _context = context;         _userManager = userManager;         _roleManager = roleManager;     }
    public async Task<bool> DashboardExistsForAsync(Guid userId)     {         return await _context.DashboardExistsForAsync(userId);     }
    public async Task<DashboardDefault> GetDashboardTemplateByRoleAsync(TuxboardUser user)     {         DashboardDefault defaultDashboard = null!;
        var roleName = await GetRoles(user);         if (string.IsNullOrEmpty(roleName))         {             defaultDashboard = await _context.GetDashboardTemplateForAsync();         }
        var role = await _roleManager.FindByNameAsync(roleName);         if (role == null)             return defaultDashboard ?? await _context.GetDashboardTemplateForAsync();
        var roleDashboard = await _context.RoleDefaultDashboards             .FirstOrDefaultAsync(e => e.RoleId == role.Id);         if (roleDashboard != null)         {             defaultDashboard =                 (await _context.GetDashboardDefaultAsync(roleDashboard.DefaultDashboardId))                 ?? null!;         }
        return defaultDashboard ?? await _context.GetDashboardTemplateForAsync();     }
    private async Task<string> GetRoles(TuxboardUser user)     {         // *COULD* have more than one role; we just want the first one.         var roles = await _userManager.GetRolesAsync(user);         return (roles.Count == 1             ? roles.FirstOrDefault()             : string.Empty)!;     } }

The GetDashboardTemplateByRoleAsync() method is what performs the heavy lifting for our service.

First, we check to see if the user is associated with any roles from Identity. If they don't have a role associated to them, we load the default dashboard.

Next, we located the role name. If we don't return a role, again, we load the default dashboard.

We, then, check to see if there is an existing dashboard associated with a role. If not, once again, we load the default dashboard. If there is a default dashboard assigned to a role, return it.

The concept behind this method is to always return a default dashboard.

Calling the RoleDashboardService

With our newly created RoleDashboardService created, we can add it to our Index.cshtml.cs file (changes in bold).

public class IndexModel : PageModel
{
    private readonly ILogger<IndexModel> _logger;
    private readonly IDashboardService _service;
    private readonly IRoleDashboardService _roleDashboardService;
    private readonly UserManager<TuxboardUser> _userManager;
    private readonly TuxboardConfig _config;
    public Dashboard Dashboard { get; set; } = null!;
    public bool HasDashboard => Dashboard != null;
    public IndexModel(
        ILogger<IndexModel> logger,
        IDashboardService service,
        IRoleDashboardService roleDashboardService,
        UserManager<TuxboardUser> userManager,
        IOptions<TuxboardConfig> options)
    {
        _logger = logger;
        _service = service;
        _roleDashboardService = roleDashboardService;
        _userManager = userManager;
        _config = options.Value;
    }
    public async Task OnGet()
    {
        var id = GetIdentity();
        if (id != Guid.Empty)
        {
            Dashboard = await _service.DashboardExistsForAsync(id)
                ? await _service.GetDashboardForAsync(_config, id)
                : await GetDashboardByRole(id);
        }
    }
    private async Task<Dashboard> GetDashboardByRole(Guid id)
    {
        var user = await _userManager.FindByIdAsync(id.ToString());
        // If we can't find the user, load the default dashboard.
        if (user == null) 
            return await _service.GetDashboardAsync(_config);
        var template = await _roleDashboardService.GetDashboardTemplateByRoleAsync(user);
        await _service.CreateDashboardFromAsync(template, id);
        return await _service.GetDashboardForAsync(_config, id);
    }
.
.

While our OnGet() contains a bit more code, our new service makes it easier to use.

If we have a user id and a dashboard exists for a user, assign the dashboard to the Dashboard property. If not, see if we can get a dashboard by a specific role.

The GetDashboardByRole method takes in a GUID (user.ID) and tries to locate the user. If it can't find it, return a default dashboard through the GetDashboardAsync(_config) method.

If we made it this far, we attempt to get a DefaultDashboard template by role. As mentioned previously, we ALWAYS return a DefaultDashboard since the next line uses the template to build a new dashboard for the user.

Finally, we return the users new dashboard.

Updating _LoginPartial.cshtml

Since we changed the Identity models, we have to update the _LoginPartial.cshtml as well.

Change this:

@inject SignInManager<IdentityUser> SignInManager
@inject UserManager<IdentityUser> UserManager

to this:

@inject SignInManager<TuxboardUser> SignInManager
@inject UserManager<TuxboardUser> UserManager

With everything in place, we can now look at the tables and how to set this up.

Configuring the Database

As mentioned above, the Tuxboard database was originally meant for software products using the Plan and WidgetPlan tables. Example subscription plans would be similar to the following:

PlanID Title
1 Platinum
2 Gold
3 Silver
4 Bronze

or

PlanID Title
1 Free
2 Premium
3 Professional
4 Enterprise

The product can contain any number of marketing tiers or levels for different experience levels of users.

When a user signs up, they select a Plan based on their needs. The PlanID is attached to a default dashboard which is set up ahead of time.

If you've jumped down to here from the top, we need a way to attach these roles to the users.

The problem with a roles approach is identifying the role of a user when they register. How do we know a user's role when they get to their dashboard? There needs to be a source of truth for the roles (and I leave that to the administrators of the system).

When a user registers on a website, they can tell you what subscription plan they want which is why the Plan table is a better for consumers.

For right now, we'll proceed with updating the database by showing you where to hit to make this work as expected.

Creating the Records

For this process, we need to create two users with passwords.

  1. Open SQL Server Management Studio (SSMS) and locate the 10-Default-Dashboards database. The database should already be created based on the "update-database" command performed at the beginning of the post.
  2. Open a new Query Window
  3. Type SELECT * FROM TuxboardRole There should be two records: Basic and Admin. Remember these two IDs for our UserRole table below.
  4. Type SELECT * FROM RoleDefaultDashboards There should also be two records in there: one for Basic and one for Admin.
  5. In Visual Studio, run the application and create two users.
  6. In SSMS, type SELECT * FROM TuxboardUser to show the two users.
  7. To relate each user to a basic role, type the following and press F5 to execute it:
    INSERT INTO TuxboardUserRole
    SELECT
    '<userid-from-tuxboarduser>' as UserId,
    '31C3DF95-FDC6-4FB5-82AB-0436EA93C1B1' as RoleId -- Basic Role
  8. For admin users, type in the following and press F5 to execute:
    INSERT INTO TuxboardUserRole
    SELECT
      '<userid-from-tuxboarduser>' as UserId,
      '7E69EB1F-07C0-46A1-B4E8-86F56386C250' as RoleId -- Admin Role

When you run the application and an administrator logs in, they'll be presented with an Administrative dashboard and a widget in the first column. When a regular user logs in, they'll see a standard dashboard with a widget in the right column.

Again, when a user logs in, they require a role to receive a dashboard. If they don't have a role assigned to them, they'll receive the default dashboard.

Conclusion

In this post, we assigned roles to default dashboards allowing users who login can see their own dashboard with custom widgets specific to their role.

We'll look at a better way to create these default dashboards in a future post.

ASP.NET 8 Best Practices on Amazon

ASP.NET 8 Best Practices by Jonathan Danylko


Reviewed as a "comprehensive guide" and a "roadmap to excellence" with over 120 Best Practices for ASP.NET Core 8, Jonathan's first book by Packt Publishing explores proven techniques for every phase of the SDLC.

Learn industry-standard concepts to improve your coding, debugging, and deployment of ASP.NET Core websites.

Order now on Amazon.com button

Picture of Jonathan "JD" Danylko

Jonathan "JD" Danylko is an author, web architect, and entrepreneur who's been programming for over 30 years. He's developed websites for small, medium, and Fortune 500 companies since 1996.

He currently works at Insight Enterprises as an Architect.

When asked what he likes to do in his spare time, he replies, "I like to write and I like to code. I also like to write about code."

comments powered by Disqus