5-Refleksion
Uge 39

Uge 39 Electronic Tech Log opbygning Iteration 1

Når man starter vores DotNet Aspire applikation som er under udvikling som ser det således ud!

Vi har altså 4 services kørende (Hvoraf den ene er boilerplate kode som giver WeatherForecast servicen)

Så vi skal reelt danne os et overblik over følgende

  • gatewayapi
  • pilotentryapi
  • webfrontend

Her er et Sekvendsdiagram over hvordan vores system arbejder med at modtage en indtastning fra en pilot.

Gateway API

Sideløbende med mit kursus har jeg holdt kontakten med gruppen og vi har i løbet af ugerne snakket om og brainstormet på hvordan vi kan skabe fundamentet for at systemet til Maersk Air Cargo får en Microservice arkitektur.

Systemet skal i første iteration kunne håndtere følgende use case: Jeg som Pilot vil have mulighed for at starte en flyvetur/entry, med preflight inspections.

Som udgangspunkt ville vi kalde det en unødig kompleksitet at introducere en microservice arkitektur i første iteration. Man kan sagtens bygge et system til denne use case som en monolit, men vi har opdaget at Microsoft har lanceret et nyt produkt som hedder .Net Aspire.

Aspire.Net skulle være en løsning til at komme hurtigt i gang med en Microservice arkitektur på en måde så den hurtigt og nemt kan shippes med Azure. Da vi læste om Aspire, virkede det som en fin løsning til at prøve kræfter med et nyt framework samtidig med at skabe fundamentet for dette system.

Kilde:
https://microservices.io/patterns/microservices.html (opens in a new tab)

https://learn.microsoft.com/en-us/dotnet/aspire/get-started/aspire-overview (opens in a new tab)

Klasser

Vi har valgt at dele alt vores data op i mindre klasser, så vi nemmere kan styre udviklingen af systemet hvis vi vil tilføje attributter til de mindre klasser. Så kan vi på denne måde gøre dette uden at risikere driftsikkerheden af hele systemet.

Til dette system benytter vi Entity Framework (Code first) til at designe modellere database strukturen. Når vi har en tabel for hver af de små "del-klasser" så betyder det at vi også beskytter samlingen af data fra onde aktører. Hvis man får adgang til tabellen som samler alle informationer om alle flyvninger så vil man kun kunne se en primary key for TechlogEntry tabellen og en foreign key for hver af de andre tabeller.

Overblikket

Nedenstående er en model af vores indledende tanker på hvordan systemet skal se ud til at starte med. Vi har defineret grund klasserne og DTO'er af flere årsager såsom afkobling af intern data fra ekstern data. Vi vil ligeledes benytte en "Gateway Api" med håbet om at det bliver smart på et senere tidspunkt at alle brugeres requests går igennem samme API og derefter vil systemet styre denne forespørgsel så brugeren får de rigtige svar.

DTO’er

Id er eksempelvis fjernet i vores DTO'er. Dette hjælper eksempelvis imod punkt 3 i OWASP 10: Sensitive Data Exposure: Hvis en klasse gemmer eller behandler følsomme data (f.eks. personlige oplysninger, legitimationsoplysninger), er det afgørende at sikre, at dataene er beskyttet mod uautoriseret adgang eller offentliggørelse.

Hvis vi eksponere id i vores requests kan det give ondsindet aktører adgang til fly logs i dette tilfælde som kan indeholde sensitive informationer om fly eller piloter som vi ikke ønsker at offentliggøre.

Datavalidering i forretningslogikken i API'en

Her vil jeg vise eksempler på Datavalidering i vores gateway API. Dette er et lag af sikkerheden overfor Injection igennem ikke autoriserede fra indtastninger fra brugere.

  • Maxlength over AircraftRegistration attributen kræver at den ikke overstiger 10 tegn.
public class AircraftDto
{
    [MaxLength(10)]
    public required string AircraftRegistration { get; set; }
 
    public string AircraftType { get; set; }
}
  • TimeOnly i Time attributen kræver at der indtastes et gyldigt tidspunkt her.
public class AircraftDto
{
   public class De_Anti_IcingDataDto
{
    public int FluidType { get; set; }
    public required string MixtureRatio { get; set; }
    public TimeOnly? Time { get; set; }
}
}

FlightDataDto'en er en samling af data på selve flyet.

  • Maxlength over FlightNumber attributen kræver at den ikke overstiger 10 tegn.
  • DateTime kræver et gyldigt datostempel.
public class FlightDataDto
{
    [MaxLength(10)]
    public required string AircraftRegistration { get; set; }
 
    [MaxLength(10)]
    public required string FlightNumber { get; set; }
    
    [Required]
    public DateTime Date { get; set; }
    
    [MaxLength(4)]
    public string? DepartureStation { get; set; }
    
    [MaxLength(4)]
    public string? DestinationStation { get; set; }
    
    [MaxLength(10)]
    public string? TookOff { get; set; }
    public string? Landed { get; set; }
}

Databasedesign med EntityFramework

I dette afsnit vil jeg vise hvordan vi har designet vores database til Gateway API'et.

Hver klasse = Skaber en ny tabel i databasen [Key] = Deifneret primary key for den pågældende tabel [Required] = Kræver at det bliver en not nullable i databasen

Når man bruger ICollection som nedenfor: Denne linje vil skabe en "Nagigation property" som Entity Framework værende en del af en én til mange relation. Entity Framework vil lede efter en foreign key i TechlogEntry klassen som svarer til Aircraft som den designer relationen til i databasen.

public class Aircraft
{
    [Key]
    [MaxLength(10)]
    public string AircraftRegistration { get; set; }
 
    [Required]
    public string AircraftType { get; set; }
 
    public ICollection<TechlogEntry> TechlogEntries { get; set; }
}
public class De_Anti_IcingData
{
    // Property to hold the FluideType enum value
    [Required]
    public int Id { get; set; }
    
    [Required]
    public int FluidType { get; set; }
    
    //public FluideType FluidType { get; set; }
    [Required]
    public string MixtureRatio { get; set; }
    
    public TimeOnly? Time { get; set; }
}
 
// Enum to represent the different fluid types
public enum FluideType
{
    Type1 = 1,
    Type2 = 2,
    Type3 = 3,
    Type4 = 4
}
public class FlightData
{
    public int Id { get; set; }
    [Required]
    [MaxLength(10)]
    public string FlightNumber { get; set; }
    [MaxLength(4)]
    public string? DepartureStation { get; set; }
    [MaxLength(4)]
    public string? DestinationStation { get; set; }
    [Required, MaxLength(10)]
    public string AircraftRegistration { get; set; }
    [Required]
    public DateTime Date { get; set; }
    public TimeOnly? TookOff { get; set; }
    public TimeOnly? Landed { get; set; }
}
public class FuelData
{
    public int Id { get; set; }
    [Required]
    public double ParkingFuel { get; set; }
    public double RevisedParkingFuel { get; set; }
    public double PlannedUplift { get; set; }
    public double ActualUplift { get; set; }
    public double FuelOnBoard { get; set; }
    public double UpliftInLiters { get; set; }
}
public class Inspection
{
    public int Id { get; set; }
    public bool PreFlightInspectionCompleted { get; set; }
    public bool PostFlightInspectionCompleted { get; set; }
}
public class Remark
{
    [Key]
    public int Id { get; set; }
    public string EntryRemark { get; set; }
}
public class TechlogEntry
{
    public int Id { get; set; }
 
    [Required]
    [MaxLength(10)]
    public string AircraftRegistration { get; set; }
 
    [ForeignKey("AircraftRegistration")]
    public Aircraft Aircraft { get; set; }
 
    public int FuelDataId { get; set; }
    public FuelData FuelData { get; set; }
 
    public int FlightDataId { get; set; }
    public FlightData FlightData { get; set; }
 
    public int? De_Anti_IcingDataId { get; set; }
    public De_Anti_IcingData? De_Anti_IcingData { get; set; }
 
    public int InspectionId { get; set; }
    public Inspection Inspection { get; set; }
 
    public int? RemarkId { get; set; }
    public Remark? Remark { get; set; }
}

Pilotentry API

Get Request til at hente entries fra piloter

Post Request til at skrive en ny pilot entry

Web frontend

Skrevet i blazor

Komponent til indtastning af pilot entry

''@page "/add-entry2"
@using System.ComponentModel.DataAnnotations
@using System.Net.Http.Json
@inject HttpClient Http
@inject AddEntryApiClient AddEntryApiClient
@rendermode InteractiveServer
 
<h3>Add Flight Entry2</h3>
 
<EditForm Model="@entry" OnValidSubmit="HandleValidSubmit" FormName="EntryForm">
    <DataAnnotationsValidator />
 
    <fieldset>
        <legend>Flight Data</legend>
 
        <div class="mb-3">
            <label for="aircraftRegistration" class="form-label">Aircraft Registration</label>
            <InputText id="aircraftRegistration" class="form-control" @bind-Value="entry.flightData.aircraftRegistration" />
            <ValidationMessage For="@(() => entry.flightData.aircraftRegistration)" />
        </div>
 
        <div class="mb-3">
            <label for="flightNumber" class="form-label">Flight Number</label>
            <InputText id="flightNumber" class="form-control" @bind-Value="entry.flightData.flightNumber" />
            <ValidationMessage For="@(() => entry.flightData.flightNumber)" />
        </div>
 
        <div class="mb-3">
            <label for="date" class="form-label">Date of Departure</label>
            <InputDate id="date" class="form-control" @bind-Value="entry.flightData.date" />
            <ValidationMessage For="@(() => entry.flightData.date)" />
        </div>
 
        <div class="mb-3">
            <label for="departureStation" class="form-label">Departure Station</label>
            <InputText id="departureStation" class="form-control" @bind-Value="entry.flightData.departureStation" />
            <ValidationMessage For="@(() => entry.flightData.departureStation)" />
        </div>
 
        <div class="mb-3">
            <label for="destinationStation" class="form-label">Destination Station</label>
            <InputText id="destinationStation" class="form-control" @bind-Value="entry.flightData.destinationStation" />
            <ValidationMessage For="@(() => entry.flightData.destinationStation)" />
        </div>
 
        <div class="mb-3">
            <label for="tookOff" class="form-label">Departure Time</label>
            <InputText id="tookOff" class="form-control" @bind-Value="entry.flightData.tookOff" />
            <ValidationMessage For="@(() => entry.flightData.tookOff)" />
        </div>
 
        <div class="mb-3">
            <label for="landed" class="form-label">Landing Time</label>
            <InputText id="landed" class="form-control" @bind-Value="entry.flightData.landed" />
            <ValidationMessage For="@(() => entry.flightData.landed)" />
        </div>
    </fieldset>
 
    <fieldset>
        <legend>Fuel Data</legend>
 
        <div class="mb-3">
            <label for="parkingFuel" class="form-label">Parking Fuel</label>
            <InputNumber id="parkingFuel" class="form-control" @bind-Value="entry.fuelData.parkingFuel" />
            <ValidationMessage For="@(() => entry.fuelData.parkingFuel)" />
        </div>
 
        <div class="mb-3">
            <label for="revisedParkingFuel" class="form-label">Revised Parking Fuel</label>
            <InputNumber id="revisedParkingFuel" class="form-control" @bind-Value="entry.fuelData.revisedParkingFuel" />
            <ValidationMessage For="@(() => entry.fuelData.revisedParkingFuel)" />
        </div>
 
        <div class="mb-3">
            <label for="plannedUplift" class="form-label">Planned Uplift</label>
            <InputNumber id="plannedUplift" class="form-control" @bind-Value="entry.fuelData.plannedUplift" />
            <ValidationMessage For="@(() => entry.fuelData.plannedUplift)" />
        </div>
 
        <div class="mb-3">
            <label for="actualUplift" class="form-label">Actual Uplift</label>
            <InputNumber id="actualUplift" class="form-control" @bind-Value="entry.fuelData.actualUplift" />
            <ValidationMessage For="@(() => entry.fuelData.actualUplift)" />
        </div>
 
        <div class="mb-3">
            <label for="fuelOnBoard" class="form-label">Fuel On Board</label>
            <InputNumber id="fuelOnBoard" class="form-control" @bind-Value="entry.fuelData.fuelOnBoard" />
            <ValidationMessage For="@(() => entry.fuelData.fuelOnBoard)" />
        </div>
 
        <div class="mb-3">
            <label for="upliftInLiters" class="form-label">Uplift in Liters</label>
            <InputNumber id="upliftInLiters" class="form-control" @bind-Value="entry.fuelData.upliftInLiters" />
            <ValidationMessage For="@(() => entry.fuelData.upliftInLiters)" />
        </div>
    </fieldset>
 
    <fieldset>
        <legend>Inspection</legend>
 
        <div class="mb-3">
            <label class="form-label">Pre-Flight Inspection Completed?</label>
            <InputCheckbox class="form-check-input" @bind-Value="entry.inspection.preFlightInspectionCompleted" />
            <ValidationMessage For="@(() => entry.inspection.preFlightInspectionCompleted)" />
        </div>
 
        <div class="mb-3">
            <label class="form-label">Post-Flight Inspection Completed?</label>
            <InputCheckbox class="form-check-input" @bind-Value="entry.inspection.postFlightInspectionCompleted" />
            <ValidationMessage For="@(() => entry.inspection.postFlightInspectionCompleted)" />
        </div>
    </fieldset>
 
    <fieldset>
        <legend>Remarks</legend>
 
        <div class="mb-3">
            <label for="remarks" class="form-label">Remarks</label>
            <InputTextArea id="remarks" class="form-control" @bind-Value="entry.remark.entryRemark" />
            <ValidationMessage For="@(() => entry.remark.entryRemark)" />
        </div>
    </fieldset>
 
    <fieldset>
        <legend>De-Anti Icing Data</legend>
 
        <div class="mb-3">
            <label for="fluidType" class="form-label">Fluid Type</label>
            <InputNumber id="fluidType" class="form-control" @bind-Value="entry.deAntiIcingData.fluidType" />
            <ValidationMessage For="@(() => entry.deAntiIcingData.fluidType)" />
        </div>
 
        <div class="mb-3">
            <label for="mixtureRatio" class="form-label">Mixture Ratio</label>
            <InputText id="mixtureRatio" class="form-control" @bind-Value="entry.deAntiIcingData.mixtureRatio" />
            <ValidationMessage For="@(() => entry.deAntiIcingData.mixtureRatio)" />
        </div>
 
        <div class="mb-3">
            <label for="time" class="form-label">Time</label>
            <InputText id="time" class="form-control" @bind-Value="entry.deAntiIcingData.time" />
            <ValidationMessage For="@(() => entry.deAntiIcingData.time)" />
        </div>
    </fieldset>
 
    <button type="submit" class="btn btn-primary">Submit</button>
</EditForm>
 
@if (submitResult != null)
{
    <div class="alert alert-success mt-3">
        @submitResult
    </div>
}
 
@code {
    private Entry entry = new Entry();
    private string submitResult;
 
    protected override async Task OnInitializedAsync()
    {
        // Mock data for initialization
        entry = new Entry
            {
                flightData = new Flightdata
                {
                    aircraftRegistration = "N12345",
                    flightNumber = "AA123",
                    date = DateOnly.Parse("2024-09-26"),
                    departureStation = "JFK",
                    destinationStation = "LAX",
                    tookOff = "14:00:00",
                    landed = "18:30:00"
                },
                fuelData = new Fueldata
                {
                    parkingFuel = 1000,
                    revisedParkingFuel = 950,
                    plannedUplift = 200,
                    actualUplift = 180,
                    fuelOnBoard = 3000,
                    upliftInLiters = 108
                },
                inspection = new Inspection
                {
                    preFlightInspectionCompleted = true,
                    postFlightInspectionCompleted = false
                },
                remark = new Remark
                {
                    entryRemark = "Smooth flight with minor turbulence."
                },
                deAntiIcingData = new DeAntiicingdata
                {
                    fluidType = 1,
                    mixtureRatio = "1:2",
                    time = "12:00:00"
                }
            };
 
        submitResult = string.Empty;
    }
 
    private async Task HandleValidSubmit()
    {
        var response = await AddEntryApiClient.PostEntryAsync(entry);
        if (response != null)
        {
            submitResult = "Entry added successfully!";
        }
        else
        {
            submitResult = "Failed to add entry.";
        }
    }
}