Skip to content

6. Backend

1Brendon edited this page Jul 7, 2023 · 6 revisions

Navigation

6.1 Structure

The backend consists of a REST API that communicates with the frontend using HTTPS POST and GET requests, as well as a MongoDB database to store data regarding the Application Tracker. The REST API was developed using the ASP .NET Web API framework. This enabled C# classes to be reused across the backend and frontend. These classes were stored in a folder named SharedModels, and were used as templates for Binary JSON (BSON) objects that were serialized and sent as the HTTPS request payload between frontend and backend. Additionally, objects related to the job application can be instantiated using the same class in both ends of the web app, which greatly decreases code replication. For example, the following class was used to model assessments for both frontend and backend.

public class Assessment
{
    //[BsonElement("Date")]
    public DateTimeOffset date { get; set; }

    //[BsonElement("Type")]
    public AssessmentType type { get; set; }

    //[BsonElement("Status")]
    public AssessmentStatus status { get; set; }

    //[BsonElement("CustomDescription")]
    public string? customDescription { get; set; }

    public bool todoScheduled { get; set; } = false;

    public string? taskId { get; set; }

    public override string ToString()
    {
        return $"date: {date}, type: {type}, status: {status}, customDescription: {customDescription}, todoScheduled: {todoScheduled}, taskId: {taskId}";
    }
}

The ASP .NET Web API framework uses Controllers to handle HTTPS requests. Only one controller class was needed in this situation, which handled requests regarding the Application Tracker. The base URL for this controller was “/api/JobApplicants".

[ApiController]
[Route("api/[controller]")]
public class JobApplicantsController : ControllerBase
{
    private readonly ILogger<JobApplicantsController> _logger;
    private readonly string _connectionString;
    private readonly MongoClient _mongoClient;
    private readonly IMongoCollection<JobApplicant> _collection;

    public JobApplicantsController(ILogger<JobApplicantsController> logger)
    {
        _logger = logger;
        _connectionString = "mongodb+srv://TESTUSER:password@graphbackend.domain.mongodb.net/
        ?retryWrites=true&w=majority";
        _mongoClient = new MongoClient(_connectionString);
        _collection = _mongoClient.GetDatabase("GraphApplication").GetCollection<JobApplicant>
        ("JobApplicationTimelines");
    }

Functions were defined within the JobApplicationController class that handled GET and POST requests to specific endpoints. The functions then query the MongoDB database using the \_mongoClient instantiated in the JobApplicationController constructor. A POST request would be handled by updating the database based on the request body, and a GET request would result in the data being fetched and then sent along with the HTTPS response. The following is an example of one of these functions, which fetches an application timeline based on its timelineID.

[HttpGet]
[Route("get-timeline/{username}/{timelineID}")]
public async Task<IActionResult> GetTimeline(string username, int timelineID)
{
    var filter = Builders<JobApplicant>.Filter.And(
        Builders<JobApplicant>.Filter.Eq("username", username));

    var applicant = await _collection.Find(filter).FirstOrDefaultAsync();

    if (applicant != null)
    {
        foreach (ApplicationTimeline timeline in applicant.applicationTimelines)
        {
            if (timeline.timelineID == timelineID)
            {
                return Ok(timeline);
            }
        }
        return NotFound();

    }
    else
    {
        return NotFound();
    }
}

6.2 MongoDB

Mongo stores documents in a specific JSON file format. The following code snippet shows the latest JSON schema representing a user’s Job Applications.

{
  "username": "string",
  "applicationTimelines": [
    {
      "timelinedID": "integer",
      "assessments": [
        {
          "date": "DateTime",
          "type": "AssessmentType",
          "status": "AssessmentStatus",
          "customDescription": "string",
          "todoScheduled": "bool",
          "taskID": "integer"
        }
      ],
      "company": "string",
      "role": "string",
      "associatedEmailAddresses": [
        "string"
      ],
      "hasUnreadEmails": "bool",
      "alertLevel": "integer",
      "archived": "bool"
    }
  ],
  "timelineCounter": "integer"
}

This schema directly translates to the C# JobApplicant class defined in SharedModels. This way, serialisation and deserialisation of data between the backend and mongo DB can be done automatically using BSON serialisation.

public class JobApplicant
{
    [BsonId]
    [BsonRepresentation(BsonType.String)]
    //[BsonElement("Username")]
    public string username { get; set; }

    //[BsonElement("ApplicationTimelines")]
    public List<ApplicationTimeline> applicationTimelines { get; set; }

    //[BsonElement("TimelineCounter")]
    public int timelineCounter { get; set; }
}

6.3 Authentication

The Microsoft.AspNetCore.Components.WebAssembly.Authentication package provides authentication and authorisation capabilities for Blazor WebAssembly applications. It enables workflows within the client-side Blazor application. It was chosen to be the main authentication mechanism as it provides a set of pre-built components and services that handle the underlying protocols and workflows, allowing developers to focus on the core functionality of their application.\

However, an issue arose when trying to incorporate Graph Toolkit into the application as it required a custom provider (https://learn.microsoft.com/en-us/graph/toolkit/providers/custom) to provide an access token which allows the toolkit components to show user information. To solve this issue, an Javascript script was used to pass the access token into the toolkit authentication. The accessToken was retrieved using the Microsoft.AspNetCore.Components.WebAssembly.Authentication library and passed into the Interop script using the JSRuntime function shown below.

await JSRuntime.InvokeVoidAsync("mgtInterop.configureProvider", AccessToken);

window.mgtInterop = {
  configureProvider: (accessToken) => {
    if (accessToken) {
      let provider = new mgt.SimpleProvider((scopes) => {
        return Promise.resolve(accessToken);
      });

      if (!mgt.Providers.globalProvider) {
        mgt.Providers.globalProvider = provider;
        mgt.Providers.globalProvider.setState(mgt.ProviderState.SignedIn);
      }
    }
  },
};

6.4 PWA

Nexus can also be access through a phone and to incorporate different screen widths @media queries were used to rearrange grid items, tiles and arrange everything in a drop-down column format.

The existing Blazor WASM can also be converted into an installable PWA[46] by using the ServiceWorkerAssetsManifest build property which lists all the files in the published output and produces a hash to catalogue if the file has changed. Blazor also provides a servide-worker.published.js file which contains a cache first falling back network strategy. The offline support is only enabled for published apps, which means that the original service-worker.js file is replaced by the service-worker.published.js.

The installing event will be triggered when a new version of the PWA assets are published. When the installation process is complete the new service worker version will be in waiting state, which is waiting for the user to close all other instances of the PWA to activate the new version. This happens automatically in Nexus. Every time the user makes a FETCH request, the fetch event is triggered, and which will look in the cache first, if the request item is present, it will return it without triggering any network request.

self.importScripts('./service-worker-assets.js');
self.addEventListener('install', event => event.waitUntil(onInstall(event)));
self.addEventListener('activate', event => event.waitUntil(onActivate(event)));
self.addEventListener('fetch', event => event.respondWith(onFetch(event)));


async function onInstall(event) {
    console.info('Service worker: Install');

    // Fetch and cache all matching items from the assets manifest
    const assetsRequests = self.assetsManifest.assets
        .filter(asset => offlineAssetsInclude.some(pattern => pattern.test(asset.url)))
        .filter(asset => !offlineAssetsExclude.some(pattern => pattern.test(asset.url)))
        .map(asset => new Request(asset.url, { integrity: asset.hash, cache: 'no-cache' }));
//#if(IndividualLocalAuth && Hosted)

    // Also cache authentication configuration
    assetsRequests.push(new Request('_configuration/ComponentsWebAssembly-CSharp.Client'));

//#endif
    await caches.open(cacheName).then(cache => cache.addAll(assetsRequests));
}

async function onActivate(event) {
    console.info('Service worker: Activate');

    // Delete unused caches
    const cacheKeys = await caches.keys();
    await Promise.all(cacheKeys
        .filter(key => key.startsWith(cacheNamePrefix) && key !== cacheName)
        .map(key => caches.delete(key)));
}

async function onFetch(event) {
    let cachedResponse = null;
    if (event.request.method === 'GET') {
        // For all navigation requests, try to serve index.html from cache,
        // unless that request is for an offline resource.
        // If you need some URLs to be server-rendered, edit the following check to exclude those URLs
//#if(IndividualLocalAuth && Hosted)
        const shouldServeIndexHtml = event.request.mode === 'navigate'
            && !event.request.url.includes('/connect/')
            && !event.request.url.includes('/Identity/')
            && !manifestUrlList.some(url => url === event.request.url);
//#else
        const shouldServeIndexHtml = event.request.mode === 'navigate'
            && !manifestUrlList.some(url => url === event.request.url);
//#endif

        const request = shouldServeIndexHtml ? 'index.html' : event.request;
        const cache = await caches.open(cacheName);
        cachedResponse = await cache.match(request);
    }

    return cachedResponse || fetch(event.request);
}

6.5 Deployment

To finalise the application it needed to be deployed so it can be accessed from the web rather than through local host. Doing this involved creating a Web Deployment Azure App Service through the Azure Active Directory and then publishing the app through Visual Studio. This worked for the main Nexus app but the backend failed to publish on its own, meaning Application Tracker wouldn't work in the published version of the app.

The front end is published to https://nexusic.azurewebsites.net/ but can only be accessed by an account in the Azure Active Directory it is configured for due to account permissions.

Clone this wiki locally