Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / ASP.NET / ASP.NET-Core

Dynamic Dashboard Web Application using ASP.NET Core, JavaScript, PostegreSql and ChartJs

5.00/5 (5 votes)
12 Feb 2021CPOL11 min read 38.3K   731  
Steps to create a nice dashboard web page using .NET MVC Core
Through this article, we will build from scratch a nice dashboard web page using .NET MVC Core project. In the steps outlined here, you will learn about clean architecture, entity framework code first approach, developing a web service and using chartjs.

Introduction

In the professional domain, CEO or qualified manager of a company prefers to have rapid access to all keys data points that help to analyse, compare and take a pertinent decision.

A dashboard is a way to have a global vision about essential data for a company, some use cases of these dashboards are comparing between net sales for two specific years, number of subscribers on a website per month for year and number of subscription sales for an Azure tenant. A dashboard is represented generally by charts and tables.

There are a lot of JavaScript libraries that help to build a nice graphic visual that can be used for, and the best of them is ChartJs.

Through this article, we will build a beautiful dashboard web application that shows some metrics about subscribed users. This application will be built using C#, ASP.NET MVC Core, JavaScript and ChartJs library.

Following this article, you will learn more about:

  • Clean architecture
  • Entity framework Core using code first approach
  • Dependency injection
  • PostgreSql DataBase
  • ChartJs
  • Testing API endpoints using PostMan

Prerequisites

To understand this article, you should have basic knowledge about ASP.NET MVC Core and JavaScript.

Create Project Architecture

In this tutorial, we will adopt the clean architecture principles to develop our application from scratch.
Adopting clean architecture, our application will have a lot of gain in maintainability and testability, thanks to separation of concerns and it will not focus on specific frameworks or technologies but on domain logic. I recommend you to visit this link and this link to have a complete definition and to learn in depth about such best practice.

Our project will be composed of four parts:

  • UI: is composed of:
    • interactor: It intercepts requests sent by the presentation layer, executes the associated scenario and returns the right result to be displayed by the view. For our example, it's an API controller.
    • presentation layer: It constitutes the GUI part, it can be developed by any framework like Angular, AngularJs, ASP.NET MVC Core, etc. For our example, it will be an ASP.NET MVC Core project.
  • Application logic: set of workflows (use cases) that implement our business rules. Their main purpose is to receive request models from controllers and transform it to a result which it passes back to view. In our case, it will be a .NET Core library project.
  • Domain: set of models or entities that reference our business logic. It should be independent from the framework. In our case, it will be a .NET Core library project.
  • Infrastructure: It contains the way in which to manage and collect data from external data sources like database, services, libraries or files. Infrastructure uses domain classes to interact with external data sources and gathers the response data. For our application, it will contain repositories and configurations to exchange with PostgreSql DataBase. In our case, it will be a .NET Core library project.

The picture below shows a snapshot of the final project structure:

Image 1

Create Database

Thanks to the use of the EF Framework, our application will be independent of database, we can plug in any type of database like PostgreSql, Oracle or SqlServer DataBase, we just need to change only the provider.

For this application, we will use PostgreSql, first we need to install it on our local machine and create a new and empty database, to do that:

  • Download and install pgadmin4 from this link.
  • Download and install pgAgent from this link.
  • Create an empty database: We need to start the pgadmin application and create a new database as follows:

Image 2

Implement BackEnd

I) Create DataAccess

DataAccess is a set of classes and configurations that ensure the dialog between application and database. Its main responsibilities are to define business entities, operate CRUD operations and translate the application data requests to some instructions known by the database server and vice versa.

The communication is ensured by using one of the following technologies or frameworks : ADO.NET, EF, NHibernate, etc., all of them have the same main goals which are making easier and transparent the process of dialog between application and database.

For our application, we will use EF (Entity Framework), the most popular ORM used in .NET Core projects that offers multiple advantages such as:

  • mapping between domain classes and relational data.

  • introduce more abstraction to manage and collect data in databases by using Linq to entities.

  • can support different relational database systems like PostgreSql, Oracle and SqlServer.

  • offer multiple approaches like code first, database first, model first.

  • data can be loaded on demand thanks to lazy loading mechanisms.

  • compared to old data access technologies like ADO.NET, the EF ORM makes it easier to read and write data from databases thanks to the mapping process, users will focus more on how to develop the business logic but not on the build of queries. It saves a considerable developing time.

  • In this section, we will focus on how to create a link between our models and database entities, create database schema using EF code first approach and prepare a dataset for our demo.

1) Create Entities and Relations

The schema of our database will be composed of the following entities:

  • User: is represented by first name, age, job and gender.
  • Profession: is the job that a user can have like dentist, software developer, teacher, etc.

The below class diagram will clearly describe the relationship between these entities and the list of properties for each table:

Image 3

These major classes will be created inside Entities folder of DashBoardWebApp.Domain project:

  • Create User class:
    C#
    public class User
      {
          public int? Id { get; set; }
          public string FirstName { get; set; }
          public int Age { get; set; }
          public string Gender { get; set; }
          public DateTime CreatedAt { get; set; }
          public int ProfessionId { get; set; }
          public Profession Profession { get; set; }
      }
    
  • Create Profession class:
    C#
    public class Profession
        {
            public int Id { get; set; }
            public string Name { get; set; }
            public List<User> Users { get; set; }
        }

2) SetUp Database

To ensure the mapping between application models and database entities, we have to follow the steps below:

  • First, install the EF Core packages and PostegreSql provider for EF using Nuget Package Manager:

    Image 4

  • Configure the Mapping between Entities and Relational Data:

    Inside DashBoardWebApp.Infrastructure/Data/Config, we define the list of constraints and relations of each entity we want to map:

  • Create UserEntityConfiguration class:
    C#
    public class UserEntityConfiguration : IEntityTypeConfiguration<User>
       {
           public void Configure(EntityTypeBuilder<User> builder)
           {
               builder.ToTable("User");
    
               builder.Property(u => u.Id)
               .ValueGeneratedOnAdd()
               .HasColumnType("serial")
               .IsRequired();
    
               builder.HasKey(u => u.Id)
               .HasName("pk_user");
    
               builder.Property(u => u.FirstName).IsRequired();
               builder.Property(u => u.Age).IsRequired().HasDefaultValue(0);
               builder.Property(u => u.Gender).IsRequired().HasDefaultValue("Male");
               builder.Property(u => u.CreatedAt).IsRequired().HasDefaultValueSql
                               ("CURRENT_TIMESTAMP");
    
               builder.Property(u => u.ProfessionId).HasColumnType("int");
               builder.HasOne(u => u.Profession).WithMany(p => p.Users).HasForeignKey
                       (u => u.ProfessionId).HasConstraintName("fk_user_profession");
           }
       }
    
  • Create ProfessionEntityConfiguration class:

    C#
    public class ProfessionEntityConfiguration : IEntityTypeConfiguration<Profession>
    {
        public void Configure(EntityTypeBuilder<Profession> builder)
        {
            builder.ToTable("Profession");
    
            builder.HasKey(p => p.Id)
              .HasName("pk_profession");
    
            builder.HasIndex(p => p.Name).IsUnique(true).HasDatabaseName
                            ("uc_profession_name");
    
            builder.HasMany(p => p.Users).WithOne(u => u.Profession);
        }
    }
    
  • Create seed data:

    We need to create ModelBuilderExtensions class that extend the ModelBuilder functionalities by adding seed method that initialize database with data, this class wil be created inside DashBoardWebApp.Infrastructure/Data/Config:

    C#
    public static class ModelBuilderExtensions
        {
            public static void Seed(this ModelBuilder modelBuilder)
            {
                List<Profession> professions = new List<Profession>()
                {
                     new Profession() { Id = 1, Name = "Software Developer"},
                     new Profession() { Id = 2, Name = "Dentist"},
                     new Profession() { Id = 3, Name = "Physician" }
                };
                
                modelBuilder.Entity<Profession>().HasData(
                  professions
                );
    
                List<User> users = new List<User>()
                {
                     new User() { Id=1, FirstName = "O.Nasri 1", Age = 30, Gender = "Male", 
                                  ProfessionId = 1, CreatedAt = new DateTime(2019, 01, 01) },
                     new User() { Id=2, FirstName = "O.Nasri 2 ", Age = 31, Gender = "Male", 
                                  ProfessionId = 1, CreatedAt = new DateTime(2019, 01, 02) },
                     new User() { Id=3, FirstName = "O.Nasri 3", Age = 32, Gender = "Male", 
                                  ProfessionId = 1, CreatedAt = new DateTime(2019, 01, 02) },
                     new User() { Id=4, FirstName = "O.Nasri 4", Age = 33, Gender = "Male", 
                                  ProfessionId = 1, CreatedAt = new DateTime(2019, 01, 04) },
                     new User() { Id=5, FirstName = "O.Nasri 4", Age = 33, Gender = "Male", 
                                  ProfessionId = 1, CreatedAt = new DateTime(2019, 02, 05) },
    
                     new User() { Id=6, FirstName = "Sonia 1", Age = 20, Gender = "Female", 
                                  ProfessionId = 2, CreatedAt = new DateTime(2019, 04, 01) } ,
                     new User() { Id=7, FirstName = "Sonia 2", Age = 20, Gender = "Female", 
                                  ProfessionId = 2, CreatedAt = new DateTime(2019, 04, 02) } ,
                     new User() { Id=8, FirstName = "Sonia 3", Age = 20, Gender = "Female", 
                                  ProfessionId = 2, CreatedAt = new DateTime(2019, 05, 03) } ,
                     new User() { Id=9, FirstName = "Sonia 4", Age = 20, Gender = "Female", 
                                  ProfessionId = 2, CreatedAt = new DateTime(2019, 05, 04) } ,
               
                     new User() { Id=10, FirstName = "O.Nasri 1", Age = 30, Gender = "Male", 
                                  ProfessionId = 1, CreatedAt = new DateTime(2020, 01, 01) },
                     new User() { Id=11, FirstName = "O.Nasri 2 ", Age = 31, Gender = "Male", 
                                  ProfessionId = 1, CreatedAt = new DateTime(2020, 01, 02) },
                     new User() { Id=12, FirstName = "O.Nasri 3", Age = 32, Gender = "Male", 
                                  ProfessionId = 1, CreatedAt = new DateTime(2020, 01, 02) },
                     new User() { Id=13, FirstName = "O.Nasri 4", Age = 33, Gender = "Male", 
                                  ProfessionId = 1, CreatedAt = new DateTime(2020, 01, 04) },
                     new User() { Id=14, FirstName = "O.Nasri 4", Age = 33, Gender = "Male", 
                                  ProfessionId = 1, CreatedAt = new DateTime(2020, 01, 05) },
    
                     new User() { Id=15, FirstName = "Thomas 1", Age = 41, Gender = "Male", 
                                  ProfessionId = 2, CreatedAt = new DateTime(2020, 03, 01) } ,
                     new User() { Id=16, FirstName = "Thomas 2", Age = 42, Gender = "Male", 
                                  ProfessionId = 2, CreatedAt = new DateTime(2020, 03, 02) } ,
                     new User() { Id=17, FirstName = "Thomas 3", Age = 43, Gender = "Male", 
                                  ProfessionId = 2, CreatedAt = new DateTime(2020, 03, 03) } ,
                     new User() { Id=18, FirstName = "Thomas 4", Age = 44, Gender = "Male", 
                                  ProfessionId = 2, CreatedAt = new DateTime(2020, 03, 04) } ,
    
                     new User() { Id=19, FirstName = "Christophe 1", Age = 25, Gender = "Male", 
                                  ProfessionId = 3, CreatedAt = new DateTime(2020, 05, 01) },
                     new User() { Id=20, FirstName = "Christophe 2", Age = 26, Gender = "Male", 
                                  ProfessionId = 3, CreatedAt = new DateTime(2020, 05, 02) },
                     new User() { Id=21, FirstName = "Christophe 3", Age = 27, Gender = "Male", 
                                  ProfessionId = 3, CreatedAt = new DateTime(2020, 05, 03)},
    
                     new User() { Id=22,  FirstName = "Linda 1", Age = 18, Gender = "Female", 
                                  ProfessionId = 1, CreatedAt = new DateTime(2020, 06, 01) },
                     new User() { Id=23,  FirstName = "Linda 2 ", Age = 19, Gender = "Female", 
                                  ProfessionId = 1, CreatedAt = new DateTime(2020, 06, 02) },
                     new User() { Id=24, FirstName = "Linda 3", Age = 20, Gender = "Female", 
                                  ProfessionId = 1, CreatedAt = new DateTime(2020, 06, 02) },
                     new User() { Id=25, FirstName = "Linda 4", Age = 21, Gender = "Female", 
                                  ProfessionId = 1, CreatedAt = new DateTime(2020, 06, 04) },
                     new User() { Id=26, FirstName = "Linda 4", Age = 22, Gender = "Female", 
                                  ProfessionId = 1, CreatedAt = new DateTime(2020, 06, 05) },
    
                     new User() { Id=27, FirstName = "Dalida 1", Age = 40, Gender = "Female", 
                                  ProfessionId = 2, CreatedAt = new DateTime(2020, 09, 06) } ,
                     new User() { Id=28, FirstName = "Dalida 2", Age = 41, Gender = "Female", 
                                  ProfessionId = 2, CreatedAt = new DateTime(2020, 09, 07) } ,
                     new User() { Id=29, FirstName = "Dalida 3", Age = 42, Gender = "Female", 
                                  ProfessionId = 2, CreatedAt = new DateTime(2020, 09, 08) } ,
                     new User() { Id=30, FirstName = "Dalida 4", Age = 43, Gender = "Female", 
                                  ProfessionId = 2, CreatedAt = new DateTime(2020, 09, 09) } ,
                };
    
                modelBuilder.Entity<User>().HasData(
                    users
               );
            }
        }
  • Create the mapping using dbContext:

    C#
    public class BDDContext : DbContext
        {
            public BDDContext([NotNullAttribute] DbContextOptions options) : base(options)
            {
            }
    
            public DbSet<User> Users { get; set; }
            public DbSet<Profession> Professions { get; set; }
    
            #region Required
            protected override void OnModelCreating(ModelBuilder modelBuilder)
            {
                modelBuilder.UseSerialColumns();
    
                modelBuilder.ApplyConfiguration<User>(new UserEntityConfiguration());
                modelBuilder.ApplyConfiguration<Profession>
                             (new ProfessionEntityConfiguration());
    
                modelBuilder.Seed();
            }
            #endregion
        }
  • Execute the migration process:

    In this step, we want to create or update the database structure after each modifications realised on model, and populate the database with a set of data if it was empty. All of this can be done with EF migration tools.

    To do that, we need to modify the content of Program class as follows:

    C#
    public class Program
     {
         public static void Main(string[] args)
         {
             var host = CreateHostBuilder(args).Build();
    
             using (var scope = host.Services.CreateScope())
             {
                 var db = scope.ServiceProvider.GetRequiredService<BDDContext>();
                 db.Database.Migrate();
             }
    
             host.Run();
         }
    
         public static IHostBuilder CreateHostBuilder(string[] args) =>
             Host.CreateDefaultBuilder(args)
                 .ConfigureWebHostDefaults(webBuilder =>
                 {
                     webBuilder.UseStartup<Startup>();
                 });
     }
    

    and by using dotnet migration tools, we can run the following command inside Infrastructure project:

    C#
    dotnet ef migrations add InitialCreate --output-dir Migrations

    Finally, when we start the Visual Studio project, our model and data will be created inside the selected database.

    Now at this level, we can use Linq to Entities for executing queries to database and retrieving data.

3) Create Repositories

This pattern introduces more abstraction about querying and managing data from databases. It’s considered like a main entry point to the dataAccess part and contains different methods that make CRUD or more complexe operations to datasource.

Each repository can manage only one database entity and it will use dbcontext and linqToEntity to query mapped object (our business entity).

The implementation will be into Infrastructure project because it depends on external source, and will be exposed to the other projects using interfaces and dependency injection (DI).

  • Create generic repository class:

    Inside DashBoardWebApp.Infrastructure/Data folder, we create a Repository class. This class will contain all common access data methods for all specific repositories, we can list:

    • Delete(TEntity entity): Remove an entity from database context, this action will be applied to the database after calling context.savechanges().
    • GetAll(Expression<Func<TEntity, bool>> filter = null, List<string> propertiesToInclude = null): return all entities that match with condition passed by filter argument. The returned result can include all specified relations specified by the ‘propertiesToInclude’ argument, thanks to Entity Framework eager loading.
    • Insert(TEntity entity): add a new entity data to database context and when we call context.savechanges() method, the object will be created into database.
    • Update(TEntity entity): update an existing entity data and when we call context.savechanges() method, the object will be updated into database.
    C#
    public class Repository<TEntity> where TEntity : class
        {
            internal BDDContext context;
            internal DbSet<TEntity> dbSet;
    
            public Repository(BDDContext context)
            {
                this.context = context;
                this.dbSet = context.Set<TEntity>();
            }
    
            /// <summary>
            /// remove entity if exists.
            /// </summary>
            /// <param name="entity"></param>
            public virtual void Delete(TEntity entity)
            {
                this.dbSet.Remove(entity);
            }
    
            /// <summary>
            /// return all entities that match with condition passed by filter argument. 
            /// The result will include all specified relations specified by the 
            /// propertiesToInclude argument.
            /// </summary>
            /// <param name="filter">where condition</param>
            /// <param name="propertiesToInclude">list of relation can be eager loaded</param>
            /// <returns></returns>
            public virtual List<TEntity> GetAll(Expression<Func<TEntity, bool>> filter = null, 
                   List<string> propertiesToInclude = null)
            {
                var query = this.dbSet.AsQueryable();
    
                if (propertiesToInclude != null && propertiesToInclude.Count > 0)
                {
                    propertiesToInclude.ForEach(p =>
                    {
                        query = query.Include(p);
                    });
                }
    
                if (filter != null)
                {
                    return query.Where(filter).ToList();
                }
                else
                {
                    return query.ToList();
                }
            }
    
            /// <summary>
            /// create a new entity
            /// </summary>
            /// <param name="entity"></param>
            public virtual void Insert(TEntity entity)
            {
                this.dbSet.Add(entity);
            }
    
            /// <summary>
            /// update an existing entity. 
            /// </summary>
            /// <param name="entity"></param>
            public virtual void Update(TEntity entity)
            {
                this.dbSet.Update(entity);
            }
        }
  • Implement UserRepository:

    Inside DashBoardWebApp.Domain/Repositories folder, create IUserRepository interface to define the list of required access data methods which are useful to implement business rules inside the application logic project. These methods are:

    • GetUsersByYear(int year): return all users created in a specific year.
    • GetAllCreatedUsersYears(): return all created users years. This information is useful to build a filter of years that get created users data for a specific year.
    C#
    public interface IUserRepository
      {
          List<User> GetUsersByYear(int year);
          List<int> GetAllCreatedUsersYears();
      }
    

    After that, we create UserRepository.cs inside DashBoardWebApp.Infrastructure/Data/Repositories folder. This class should reuse common methods from Repository class and implement methods declared by IUserRepository interface:

    C#
    public class UserRepository : Repository<User>, IUserRepository
        {
            private readonly BDDContext _context;
    
            public UserRepository(BDDContext context) : base(context)
            {
                this._context = context;
            }
    
            public List<User> GetUsersByYear(int year)
            {
                Expression<Func<User, bool>> filterByYear = (u) => u.CreatedAt.Year == year;
    
                List<String> propertiesToInclude = new List<string>() { "Profession" };
                return base.GetAll(filterByYear, 
                       propertiesToInclude)?.OrderBy(u => u.CreatedAt).ToList();
            }
    
            public List<int> GetAllCreatedUsersYears()
            {
                return this.dbSet?.Select
                       (u => u.CreatedAt.Year).Distinct().OrderBy(y => y).ToList();
            }
        }

    Once we finish the implementation of repositories, we can use them to build our application logic? But, before that, we need to declare it into the Dependency Injection (DI) system by modifying ConfigureServices method of Startup class:

    C#
    public void ConfigureServices(IServiceCollection services)
       {
           services.AddControllersWithViews();
           services.AddDbContext<BDDContext>(
             options => options.UseNpgsql("Host=localhost; user id=postgres;
                        password=YOUR_PASSWORD; database=DashboardBDD"));
    
           services.AddScoped<IUserRepository, UserRepository>();
       }
    

II) Implement Application Logic

The use case we want to implement is to retrieve three kinds of data:

  • The first one is to retrieve subscribed users for specific year grouped by month, and this data can be projected inside the line chart component.

  • The second one is to retrieve subscribed users for specific year grouped by profession, and this data will be displayed inside a pie chart.

  • The third one is to retrieve subscribed users for specific year grouped by age, this data will be projected inside a pie chart.

To do the implementation, we need to create our view models classes:

  • Create LineChartDataDTO model inside DashBoardWebApp.Application/common/DTO:

    It holds the x, y coordinates of a point inside a Line chart.

    C#
    public class LineChartDataDTO
     {
         public DateTime X { get; set; }
         public decimal Y { get; set; }
    
         public LineChartDataDTO()
         {
    
         }
    
         public LineChartDataDTO(DateTime x, int y)
         {
             this.X = x;
             this.Y = y;
         }
     }
    
  • Create PieChartDataDTO model inside DashBoardWebApp.Application/common/DTO:

    It holds the label and percentage value of a slice in Pie chart.

    C#
    public class PieChartDataDTO
        {
            public string Label { get; set; }
            public decimal Value { get; set; }
    
            public PieChartDataDTO()
            {
    
            }
    
            public PieChartDataDTO(string label, decimal value)
            {
                Label = label;
    
                Value = Math.Round(value, 2);
            }
        }
  • Create DashBoardDTO model inside DashBoardWebApp.Application/UseCases/DashBoard/DTO:

    This model contains all data required by client side, it holds:

    • List of all years of users creation
    • List of subscribed users in a specific year grouped by month
    • List of subscribed users in a specific year grouped by gender
    • List of subscribed users in a specific year grouped by profession
    C#
    public class DashBoardDTO
       {
           public List<int> Years { get; set; }
           public List<LineChartDataDTO> SubscribedUsersForYearGroupedByMonth { get; set; }
           public List<PieChartDataDTO> SubscribedUsersForYearGroupedByGender { get; set; }
           public List<PieChartDataDTO>
                  SubscribedUsersForYearGroupedByProfession { get; set; }
       }
    
  • Create IDashboardService interface inside DashBoardWebApp.Application/UseCases/DashBoard/services:

    C#
    public interface IDashboardService
       {
           DashBoardDTO GetSubscribedUsersStatsByYear(int? year);
       }
    
  • Create DashboardService inside DashBoardWebApp.Application/UseCases/DashBoard/services:

    This class contains the implementation of different methods exposed by the contract.

    C#
    public class DashboardService : IDashboardService
       {
           private IUserRepository _userRepository;
           public DashboardService(IUserRepository userRepository)
           {
               this._userRepository = userRepository;
           }
    
           public DashBoardDTO GetSubscribedUsersStatsByYear(int? year)
           {
               DashBoardDTO dashBoard = new DashBoardDTO();
    
               dashBoard.Years = this._userRepository.GetAllCreatedUsersYears();
    
               if (dashBoard.Years == null || dashBoard.Years.Count == 0)
               {
                   return dashBoard;
               }
    
               if (!year.HasValue)
               {
                   //if year not exists then set it with the last year from years list.
                   year = dashBoard.Years.LastOrDefault();
               }
    
               List<User> subsribedUsers = this._userRepository.GetUsersByYear(year.Value);
    
               if (subsribedUsers?.Count == 0)
               {
                   return dashBoard;
               }
    
               dashBoard.SubscribedUsersForYearGroupedByMonth =
                         subsribedUsers.GroupBy(g => g.CreatedAt.Month).Select
                         (g => new LineChartDataDTO(g.First().CreatedAt, g.Count())).ToList();
    
               var totalCount = subsribedUsers.Count;
    
               dashBoard.SubscribedUsersForYearGroupedByGender = subsribedUsers.GroupBy
                         (g => g.Gender).Select(g => new PieChartDataDTO(g.Key, g.Count()*
                         100/(decimal)totalCount )).ToList();
               dashBoard.SubscribedUsersForYearGroupedByProfession =
                         subsribedUsers.GroupBy(g => g.Profession.Name).Select
                         (g => new PieChartDataDTO(g.Key, g.Count() *
                         100 / (decimal)totalCount )).ToList();
    
               dashBoard.SubscribedUsersForYearGroupedByGender.Last().Value =
               100 - dashBoard.SubscribedUsersForYearGroupedByGender.Where(d => d !=
               dashBoard.SubscribedUsersForYearGroupedByGender.Last()).Sum(d => d.Value);
               dashBoard.SubscribedUsersForYearGroupedByProfession.Last().Value =
               100 - dashBoard.SubscribedUsersForYearGroupedByProfession.Where
               (d => d != dashBoard.SubscribedUsersForYearGroupedByProfession.Last()).Sum
               (d => d.Value);
    
               return dashBoard;
           }
       }
    

    At the end, we declare DashboardService class inside the DI system to be instantiated automatically inside our controller on each request. We need to add the following instruction to ConfigureServices of Startup class.

    C#
    services.AddScoped<IDashboardService, DashboardService>();

III) Implement Web Service

The last step on the Back-end side is to create an entry point for the developed use case, for that, we should create a Dashboard Web API class that exposes an endpoint for our developed use case. It will contain only one method called FilterByYear that returns all information needed about subscribed users for a specific year.

C#
[Route("api/dashboard")]
    [ApiController]
    public class DashboardApi : ControllerBase
    {
        private readonly IDashboardService _dashboardService;

        public DashboardApi(IDashboardService dashboardService)
        {
            this._dashboardService = dashboardService;
        }

        [HttpGet("{year:int?}")]
        public IActionResult FilterByYear([FromRoute] int? year)
        {
            return Ok(this._dashboardService.GetSubscribedUsersStatsByYear(year));
        }
    }

IV) Test Web API

To test our web API service, we can use Postman to create and execute http REST requests:

  • Start the project from the Visual Studio.
  • Create a new REST request using Postman.

    Image 5

  • Execute the request.

    Image 6

Implement FrontEnd

  • First, we need to import chartJs and moment.js libraries into the layout page (path: Views/Home/_Layout.chtml).

  • Next, modify home page (path: Views/Home/Index.chtml) to display principal filter, line chart and two pie charts:
    C#
    @{
        ViewData["Title"] = "Home Page";
    }
    
    <div class="text-center">
        <p><h4>Developing a dashboard web application with ASP.NET MVC Core, 
               WEB Api, JavaScript, PostegreSql and ChartJs</h4></p>
        <div class="flex-d-column">
            <select id="filterByYear">
            </select>
            <div class="fullWidth">
                <canvas id="mylineChart1"></canvas>
            </div>
            <div class="flex-d-row fullWidth">
                <div class="chart-container">
                    <canvas id="mypieChart1"></canvas>
                </div>
                <div class="chart-container">
                    <canvas id="mypieChart2"></canvas>
                </div>
            </div>
        </div>
    </div>
  • Next, add some style to the home page by creating a dashboard.css file (path: wwwroot/css/dashboard.css), then include it inside layout page.
    C#
    .fullWidth {
        width: 100%
    }
    
    .flex-d-column {
        display: flex;
        flex-direction: column;
    }
    
    .flex-d-row {
        display: flex;
        flex-direction: row;
    }
    
    .chart-container {
        flex: 1;
    }
  • Then, implement actions for the Home page using JavaScript language: we need to create a dashboard.js (path: wwwroot/js/dashboard.js) that will be composed by the above list of functions:
    • drawLineChart: This function creates a pie chart configuration used to draw or update a chart on specific canvas.
    • drawPieChart: This function creates a pie chart configuration used to draw or update a chart on specific canvas.
    • drawChart: Using settings created by the above functions, it will update an existing instance of chart if it exists or create a new graphic on a specific canvas and return a new instance that will be used for future update.
    • makeRandomColor: will return random hexadecimal color. This function attributes a random color to a different chart on each update action.
    • filterDashboardDataByYear: This is our main function, it will fire after each change detected on global year filter, it will send a request to dashboard API and get a response data() that will be displayed into dedicated charts.
    C#
    $(document).ready(function () {
        let lineChart1 = null;
        let pieChart1 = null;
        let pieChart2 = null;
    
        function drawChart(chartInstance, canvasId, chartSettings) {
           
            if (chartInstance != null) {
                //update chart with new configuration
                chartInstance.options = { ...chartSettings.options };
                chartInstance.data = { ...chartSettings.data };
     
                chartInstance.update();
                return chartInstance;
            } else {
                //create new chart.
                var ctx = document.getElementById(canvasId).getContext('2d');
                return new Chart(ctx, chartSettings);
            }
        }
    
        function buildSelectFilter(years, currentYear) {
            //clear all options.
            $("#filterByYear").empty();
            var selectOptionsFilterHtml = "";
    
            if (years) {
                years.forEach((year) => {
                    selectOptionsFilterHtml += `<option value="${year}" 
                          ${currentYear == year ? 'selected':''}>${year}</option>`
                });
            }
    
            $("#filterByYear").append(selectOptionsFilterHtml);
        }
    
        function makeRandomColor() {
            return "#" + Math.floor(Math.random() * 16777215).toString(16);
        }
    
        function drawLineChart(chartInstance, canvasId, data, titleText) {
    
            let settings = {
                // The type of chart we want to create
                type: 'line',
                // The data for our dataset
                data: {
                    datasets: [{
                        backgroundColor: 'rgba(255,0,0,0)',
                        borderColor: makeRandomColor(),
                        data: data
                    }]
                },
    
                // Configuration options go here
                options: {
                    legend: {
                        display: false
                    },
                  
                    title: {
                        display: true,
                        text: titleText,
                        fontSize: 16
                    },
                    scales: {
                        xAxes: [{
                            type: 'time',
                            time: {
                                unit: 'month',
                                displayFormats: {
                                    month: 'MM YYYY'
                                }
                            }
                        }]
                    }
                }
            };
    
            return drawChart(chartInstance, canvasId, settings);
        }
    
        function drawPieChart(chartInstance, canvasId, data, labels, titleText) {
    
            //generate random color for each label.
            let bgColors = [];
    
            if (labels) {
                bgColors = labels.map(() => {
                    return makeRandomColor();
                });
            }
    
            var settings = {
                // The type of chart we want to create
                type: 'pie',
    
                // The data for our dataset
                data: {
                    labels: labels,
                    datasets: [{
                        backgroundColor: bgColors,
                        borderColor: bgColors,
                        data: data
                    }],
                },
    
                // Configuration options go here
                options: {
                    tooltips: {
                        callbacks: {
                            label: function (tooltipItem, data) {
                                //create custom display.
                                var label = data.labels[tooltipItem.index] || '';
                                var currentData = data.datasets[0].data[tooltipItem.index];
    
                                if (label) {
                                    label = `${label} ${Number(currentData)} %`;
                                }
    
                                return label;
                            }
                        }
                    },
                    title: {
                        display: true,
                        text: titleText,
                        fontSize: 16
                    },
                }
            };
    
            return drawChart(chartInstance, canvasId, settings);
        }
    
        function filterDashboardDataByYear(currentYear) {
    
            currentYear = currentYear || '';
            let url = `http://localhost:65105/api/dashboard/${currentYear}`;
    
            $.get(url, function (data) {
    
                if (!currentYear && data.years.length > 0) {
                    //pick the last year.
                    currentYear = data.years.reverse()[0];
                }
    
                buildSelectFilter(data.years, currentYear);
                
                let data1 = [];
                if (data.subscribedUsersForYearGroupedByMonth) {
                    data1 = data.subscribedUsersForYearGroupedByMonth.map
                            (u => { return { "x": moment(u.x, "YYYY-MM-DD"), "y": u.y } });
                }
    
                let data2 = [];
                let labels2 = []; 
                if (data.subscribedUsersForYearGroupedByGender) {
                    data2 = data.subscribedUsersForYearGroupedByGender.map(u => u.value);
                    labels2 = data.subscribedUsersForYearGroupedByGender.map(u => u.label);
                }
    
                let data3 = [];
                let labels3 = [];
                if (data.subscribedUsersForYearGroupedByProfession) {
                    data3 = data.subscribedUsersForYearGroupedByProfession.map(u => u.value);
                    labels3 = 
                    data.subscribedUsersForYearGroupedByProfession.map(u => u.label);
                }
              
                lineChart1 = drawLineChart(lineChart1, "mylineChart1", data1, 
                             `Number of subscribed users per month in ${currentYear}`);
                pieChart1 = drawPieChart(pieChart1, "mypieChart1", data2, labels2, 
                            `Number of subscribed users in ${currentYear} 
                             grouped by gender`);
                pieChart2 = drawPieChart(pieChart2, "mypieChart2", data3, labels3, 
                            `Number of subscribed users in 
                             ${currentYear} grouped by profession`);
            });
        }
    
        filterDashboardDataByYear();
    
        $(document).on("change", "#filterByYear", function () {
            filterDashboardDataByYear(parseInt($(this).val()));
        });
    });     

This JS file should be imported into a layout page.

Run Application

When we run the application at the first time, the displayed data will be filtered based on the last year of created users.

Image 7

We can choose a different year from combobox to show another users statistics related to the selected year.

Image 8

References

Points of Interest

I hope you appreciated this article. Thank you for viewing my post, try to download the source code and do not hesitate to leave your questions and comments.

History

  • v1 24th January, 2021: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)