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.
- 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.
- 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.
- 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.
- 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.
- 3 directions each with 5 subjects functioning as the competition mould.
- Upon creation a competition is associated with a
- 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
- Optional account activation and password reset
- Emailing enabled and server settings configured in appsettings.json.
- Bot protection with Google’s reCAPTCHA v3.
- Captcha key and secret configured in appsettings.json.
- 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
- 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).
- 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.
- 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.
- Originally an application does not have an
- 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.
- 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.
- 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.
- 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
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.