Admissions App with ASP.NET Core MVC

This post contains the implementation details of the web application created for managing the admission competition in the Greek National School of Judges (GNSJ). For a description of the application itself please visit the relevant portfolio item.

Key technologies:

  • ASP.NET Core MVC on .NET Core as the main application framework.
  • Entity Framework Core ORM for object-oriented, type-safe database access.
  • MySQL database server with Pomelo.EntityFrameworkCore.MySQL EF Core provider.
  • DinkToPdf/wkhtmltopdf for dynamic report creation in pdf format.
  • Automapper for view model <-> entity mapping.
  • Serilog for application logging.
  • JQuery/unobtrusive javascript for UI functionality.

High-level architecture:

  • Three main projects:
    • Esdi.Entities (database mappings): 38 POCOs, including multiple not-mapped calculation properties.
    • Esdi.Services (service layer with business logic):  11 Services, > 150 methods, EF database context.
    • Esdi (MVC application): 2 Areas, 16 controllers, ~150 views, database seeding, various helpers.
  • Esdi never accesses the database directly but only through Esdi.Services.
    • Each controller is injected with the services it requires.
  • Separate Areas for internal and external functionality accessed at separate route prefixes:
    • Internal route prefix is blocked at the web server from outside access.
  • External area (3 controllers, ~ 30 views):
    • User registration.
    • Application submission.
  • Internal area (12 controllers, ~ 120 views):
    • Role-based user management system for internal users.
    • Competition setup and progress monitoring.
    • Application examination and approval workflow.
    • Exam management with automatic room assignment.
    • Grades entry, results reporting and exporting.

Database initialization:

  • The database schema is automatically initialized (or upgraded) at application start (using EF Core migrations).
  • Initial database values are also inserted (or updated) at application start. These include:
    • Lookup values (e.g. countries, ranks, genders etc.).
    • Application data that do not often change (e.g. directions, subjects, foreign languages):
      • Saves some interfaces.
      • Allows for quick application maintenance.
  • Initial values are defined in .json files that can be edited outside the application.
    • Editing the .json files and restarting the application automatically updates the data.
    • Values may include an “IsActive” field in order to deactivate them without breaking database consistency.
    • All values are retrieved using a single generic lookup service method.

User Management:

  • ASP.NET Core Identity.
  • Role-based, attribute authorization mostly at the controller level (easier to manage than at the action level).
    • Anonymous users are allowed only to login/register.
    • External users are allowed to create/edit/submit application.
    • Internal users are allowed in the internal interfaces according to their role.
  • Added custom claims at sign-in in order to avoid re-querying the database in controller actions (UserId, Email).
  • All user management functionality is implemented in the corresponding service.

Competition management:

  • 3 directions each with 5 subjects functioning as the competition mould.
  • Upon creation a competition is associated with a direction.
    • Corresponding subject instances pointing to the original subjects are created.
    • The competition direction cannot change.
  • A competition is marked as active/inactive.
    • Only active competitions are available to applicants

User registration:

  • Optional account activation and password reset via email.
    • Emailing enabled and server settings configured in appsettings.json.
  • Bot protection with Google’s reCAPTCHA v3.
    • Captcha key and secret configured in appsettings.json.

Application submission:

  • Single application form per user (create the first time only and then edit).
  • External access can be configured in appsettings.json to allow login, registration, or application editing access.
  • 4-step, wizard-like application editing with saving functionality:
    • Personal information entering and file uploading pages.
    • Final submission functionality which locks application for editing.
    • Submitted application may be downloaded in pdf format.
  • 2 view models with different validation rules per page for:
    • Validity: Must be valid in order to save.
    • Completion: Must be valid in order to move on to the next page.
  • Protect access to the application pages:
    • The application belongs to the user that is logged in.
    • The application has not been submitted.
    • All previous application pages are complete (redirects to the first page with incomplete information).

File uploading:

  • Uploaded files are saved in the filesystem under /UserFiles/Application_Id.
  • Uploaded filenames are of the form: DocumentType_UploadingDate_DocumentId.
  • For each file there is a database record including the client filename, the server filename prefix and the file extension.
  • Easy to relate database record with actual file and vice versa.

PDF creation:

  • A pre-created print view works as a Razor printing template.
  • A print view model is created dynamically from the application entity.
  • The MVC render engine is used to render the Razor view with the view model data to html.
  • The html text along with the required css is converted to pdf using the DinkToPdf library.
  • Pdf file is downloaded and opened in the browser.

Application management:

  • Originally an application does not have an internal record:
    • The record is created the first time an employee accesses it.
    • Uploaded files can be opened from the browser.
  • When all application requirements are fulfilled (i.e. the related checkboxes are checked):
    • The status of the application changes to “approved”.
    • Subject instances that hold the subject grades are created.
    • The application can be reverted to its original state by the supervisor
      • Subject instances are deleted and application switched to the default state.
  • An application can be rejected at any time by a supervisor.

Exam management:

  • A number of exam rooms are created per competition subject. Exams rooms are:
    • Associated with an actual room.
    • Ordered based on an order number (allowing reordering).
    • Associated with a number of proctors.
    • Assigned a number of applicants.
      • Applicant assignment is done automatically on the client in alphabetical order, according to room capacity.
  • Exam date and time is stored with the competition subject (i.e. all exam rooms are scheduled at the same time).
  • Subject exam setup (exam rooms and applicants) can be copied to other subjects.

Grade entry:

  • Written exams:
    • In-place, per-applicant grade entry.
    • Separate pages for grading and re-grading (in case of large grade difference).
  • Oral exams:
    • Per-page grade entry in alphabetical order.

Reporting:

  • Reports are created by a reporting service:
    • Supports multiple report types via related service methods.
  • Each service method:
    • Defines the basic report characteristics such as headers, footers, colors, etc.
    • Accepts the report columns/data and produces a pdf file (as described earlier).
    • Can be used for multiple report variants.
  • Reporting service could be easily extended to support user-customizable reports.
  • Reflection-based CSV serializer service:
    • Takes an object with members of built-in types and creates CSV text.
    • We simply need to pass the desired viewmodel.

Database concurrency handling:

  • Optimistic concurrency:
    • Tables include a LastUpdatedAt field updated with every row modification.
    • Views render LastUpdatedAt at retrieval time as a hidden field.
    • LastUpdatedAt at retrieval time is set as the field’s original value before saving.
    • When saving the values are compared. If found different update does not happen.
  • Hurdle 1 (MySQL complication):
    • MySQL does not support a RowVersion Timestamp like the SQL Server (no documentation was available).
    • Solution: Use a high-precision, auto-generated DateTime field.
      • The exact EF Core attributes were discovered with experimentation.
  • Hurdle 2 (View renderer complication):
    • View renderer cuts off precision beyond seconds for DateTime objects.
    • Solution: DateTime object is converted to ticks (epoch number) before rendering the view.
      • Ticks are converted back to DateTime before updating.

Front-end interactions:

  • Wherever possible, only parts of a page are reloaded asynchronously using Ajax:
    • Searching: Intercept form submit and send form data (via post or get).
    • File uploading: Intercept form submit and send file.
    • In-place editing: Send form data by clicking a regular link.
    • Paging: Retrieve paging data by clicking the paging-related links (First, Previous, …):
      • Care is taken so that the current filter and sort order are retained.
      • The pager is included as a partial view component where applicable.
  • Ajax features are enabled using unobtrusive javascript and JQuery.
  • The action element (form, button, or anchor) is tagged with a number of data- attributes:
    • An attribute enables the asynchronous feature.
    • Another attribute determines the target element (the div that will be replaced with the result of the call).
    • Optional attributes allow for passing additional parameters where applicable.
  • Caveat:
    • When replacing part of a page the javascript code that scans for data- attributes must be re-run.
    • It must be made sure it is only run for the replaced page part.
      • Otherwise events will be set again for the remaining parts causing duplicate server requests.
    • Conclusion: Clean, asynchronous page loading awkward in an MVC application.

User notifications:

  • Automatically removed after a few seconds.
  • Four types: success, info, warning, danger.
  • Global alerts:
    • Placeholder tag helper in the layout file (present on all pages).
    • Boostrap alerts in the lower right corner of the window.
    • Uses TempData (which uses session) so they work across pages (e.g. save proctor).
  • Local alerts:
    • Optional placeholder tag helper in each view file.
    • Simple text anywhere on the page.
    • Uses ViewData so they only work on the same page.

Other features:

  • Using selectize.js to select from filtered list (helpful when the number of results is high).
  • Automapper: Heavy use of the mapping library for viewmodel <-> entity updating.
    • Involves a bit of learning curve but helps significantly with tidying up the code.
    • It may not be worth it for complex mapping scenarios with multiple layers of objects.
  • Different redirect-to-login for unauthenticated requests depending on the Area (Internal vs. External):
    • Used ConfigureApplicationCookie options trick to redirect to /Internal/Acount/Login or /Acount/Login.
  • Multi-language: Cookie-based, set when user selects language.
    • Shadow directory structure under Resources folder containing the resource files.
    • Problem with validation attributes not being translated.
    • A CustomValidationAttributeAdapterProvider had to be used to support it.
  • Same owner requirement for application editing implemented as an authorization handler.