Skip to content

Commit

Permalink
Merge pull request #16 from davewalker5/FR-24-Bug-Fixes
Browse files Browse the repository at this point in the history
FR-24 Bug-Fixes and Improvements
  • Loading branch information
davewalker5 committed Oct 22, 2023
2 parents 2814940 + 9d1bb76 commit 7d36f4c
Show file tree
Hide file tree
Showing 29 changed files with 485 additions and 33 deletions.
25 changes: 23 additions & 2 deletions src/FlightRecorder.BusinessLogic/Factory/FlightRecorderFactory.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
using System;
using System.Diagnostics.CodeAnalysis;
using FlightRecorder.BusinessLogic.Logic;
using FlightRecorder.Data;
using FlightRecorder.Entities.Interfaces;
using FlightRecorder.Entities.Reporting;

namespace FlightRecorder.BusinessLogic.Factory
{
Expand All @@ -17,7 +19,11 @@ public class FlightRecorderFactory
private readonly Lazy<IUserManager> _users = null;
private readonly Lazy<ICountryManager> _countries = null;
private readonly Lazy<IAirportManager> _airports = null;

private readonly Lazy<IDateBasedReport<AirlineStatistics>> _airlineStatistics = null;
private readonly Lazy<IDateBasedReport<LocationStatistics>> _locationStatistics = null;
private readonly Lazy<IDateBasedReport<ManufacturerStatistics>> _manufacturerStatistics = null;
private readonly Lazy<IDateBasedReport<ModelStatistics>> _modelStatistics = null;
public FlightRecorderDbContext Context { get; private set; }
public IAirlineManager Airlines { get { return _airlines.Value; } }
public ILocationManager Locations { get { return _locations.Value; } }
public IManufacturerManager Manufacturers { get { return _manufacturers.Value; } }
Expand All @@ -28,7 +34,18 @@ public class FlightRecorderFactory
public IUserManager Users { get { return _users.Value; } }
public ICountryManager Countries { get { return _countries.Value; } }
public IAirportManager Airports { get { return _airports.Value; } }
public FlightRecorderDbContext Context { get; private set; }

[ExcludeFromCodeCoverage]
public IDateBasedReport<AirlineStatistics> AirlineStatistics { get { return _airlineStatistics.Value; } }

[ExcludeFromCodeCoverage]
public IDateBasedReport<LocationStatistics> LocationStatistics { get { return _locationStatistics.Value; } }

[ExcludeFromCodeCoverage]
public IDateBasedReport<ManufacturerStatistics> ManufacturerStatistics { get { return _manufacturerStatistics.Value; } }

[ExcludeFromCodeCoverage]
public IDateBasedReport<ModelStatistics> ModelStatistics { get { return _modelStatistics.Value; } }

public FlightRecorderFactory(FlightRecorderDbContext context)
{
Expand All @@ -43,6 +60,10 @@ public FlightRecorderFactory(FlightRecorderDbContext context)
_users = new Lazy<IUserManager>(() => new UserManager(context));
_countries = new Lazy<ICountryManager>(() => new CountryManager(context));
_airports = new Lazy<IAirportManager>(() => new AirportManager(this));
_airlineStatistics = new Lazy<IDateBasedReport<AirlineStatistics>>(() => new DateBasedReport<AirlineStatistics>(context));
_locationStatistics = new Lazy<IDateBasedReport<LocationStatistics>>(() => new DateBasedReport<LocationStatistics>(context));
_manufacturerStatistics = new Lazy<IDateBasedReport<ManufacturerStatistics>>(() => new DateBasedReport<ManufacturerStatistics>(context));
_modelStatistics = new Lazy<IDateBasedReport<ModelStatistics>>(() => new DateBasedReport<ModelStatistics>(context));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<PackageId>FlightRecorder.BusinessLogic</PackageId>
<PackageVersion>1.0.5.0</PackageVersion>
<PackageVersion>1.1.1.0</PackageVersion>
<Authors>Dave Walker</Authors>
<Copyright>Copyright (c) Dave Walker 2020, 2021, 2022</Copyright>
<Copyright>Copyright (c) Dave Walker 2020, 2021, 2022, 2023</Copyright>
<Owners>Dave Walker</Owners>
<PackageReleaseNotes>Dependency updates</PackageReleaseNotes>
<Summary>Flight Recorder Business Logic</Summary>
Expand All @@ -16,7 +16,7 @@
<PackageProjectUrl>https://github.com/davewalker5/FlightRecorderDb</PackageProjectUrl>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
<ReleaseVersion>1.0.5.0</ReleaseVersion>
<ReleaseVersion>1.1.1.0</ReleaseVersion>
</PropertyGroup>

<ItemGroup>
Expand All @@ -26,6 +26,26 @@
<ItemGroup>
<Compile Remove="Base\LogicBase.cs" />
</ItemGroup>
<ItemGroup>
<None Remove="Sql\AirlineStatistics.sql" />
<None Remove="Sql\LocationStatistics.sql" />
<None Remove="Sql\ManufacturerStatistics.sql" />
<None Remove="Sql\ModelStatistics.sql" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Sql\AirlineStatistics.sql">
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
</EmbeddedResource>
<EmbeddedResource Include="Sql\LocationStatistics.sql">
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
</EmbeddedResource>
<EmbeddedResource Include="Sql\ManufacturerStatistics.sql">
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
</EmbeddedResource>
<EmbeddedResource Include="Sql\ModelStatistics.sql">
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
</EmbeddedResource>
</ItemGroup>
<ItemGroup>
<Folder Include="Factory\" />
<Folder Include="Extensions\" />
Expand Down
25 changes: 25 additions & 0 deletions src/FlightRecorder.BusinessLogic/FlightRecorder.BusinessLogic.sln
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.5.002.0
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FlightRecorder.BusinessLogic", "FlightRecorder.BusinessLogic.csproj", "{A666D73E-C9A8-4F30-863B-DED16DA1884F}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{A666D73E-C9A8-4F30-863B-DED16DA1884F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A666D73E-C9A8-4F30-863B-DED16DA1884F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A666D73E-C9A8-4F30-863B-DED16DA1884F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A666D73E-C9A8-4F30-863B-DED16DA1884F}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {28A1C7E4-5D14-478A-A674-1CE34D499690}
EndGlobalSection
EndGlobal
2 changes: 1 addition & 1 deletion src/FlightRecorder.BusinessLogic/Logic/AirportManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

namespace FlightRecorder.BusinessLogic.Logic
{
public class AirportManager : IAirportManager
internal class AirportManager : IAirportManager
{
private readonly FlightRecorderFactory _factory;

Expand Down
2 changes: 1 addition & 1 deletion src/FlightRecorder.BusinessLogic/Logic/CountryManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

namespace FlightRecorder.BusinessLogic.Logic
{
public class CountryManager : ICountryManager
internal class CountryManager : ICountryManager
{
private readonly FlightRecorderDbContext _context;

Expand Down
67 changes: 67 additions & 0 deletions src/FlightRecorder.BusinessLogic/Logic/DateBasedReport.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
using FlightRecorder.Data;
using FlightRecorder.Entities.Interfaces;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Threading.Tasks;

namespace FlightRecorder.BusinessLogic.Logic
{
[ExcludeFromCodeCoverage]
internal class DateBasedReport<T> : ReportManagerBase, IDateBasedReport<T> where T : class
{
private const string DateFormat = "yyyy-MM-dd";
private const string FromDatePlaceHolder = "$from";
private const string ToDatePlaceHolder = "$to";

internal DateBasedReport(FlightRecorderDbContext context) : base(context)
{
}

/// <summary>
/// Generate a datebased report for reporting entity type T
/// </summary>
/// <param name="from"></param>
/// <param name="to"></param>
/// <param name="pageNumber"></param>
/// <param name="pageSize"></param>
/// <returns></returns>
public async Task<IEnumerable<T>> GenerateReport(DateTime? from, DateTime? to, int pageNumber, int pageSize)
{
// SQL report files are named after the keyless entity type they map to with a .sql extension
var sqlFile = $"{typeof(T).Name}.sql";

// Load the SQL file and perform date range place-holder replacements
var query = ReadDateBasedSqlReportResource(sqlFile, from, to);

// Run the query and return the results
var results = await GenerateReport<T>(query, pageNumber, pageSize);
return results;
}

/// <summary>
/// Read the SQL report file for a sightings-based report with a date range in it
/// </summary>
/// <param name="reportFile"></param>
/// <param name="from"></param>
/// <param name="to"></param>
/// <returns></returns>
private static string ReadDateBasedSqlReportResource(string reportFile, DateTime? from, DateTime? to)
{
// Get non-NULL versions of the from and to dates
var nonNullFromDate = (from ?? DateTime.MinValue).ToString(DateFormat);
var nonNullToDate = (to ?? DateTime.MaxValue).ToString(DateFormat);

// Read and return the query, replacing the date range parameters
var query = ReadSqlResource(reportFile, new Dictionary<string, string>
{
{ FromDatePlaceHolder, nonNullFromDate },
{ ToDatePlaceHolder, nonNullToDate }

});

return query;
}

}
}
120 changes: 120 additions & 0 deletions src/FlightRecorder.BusinessLogic/Logic/ReportManagerBase.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
using FlightRecorder.Data;
using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;

namespace FlightRecorder.BusinessLogic.Logic
{
[ExcludeFromCodeCoverage]
internal abstract class ReportManagerBase
{
private readonly FlightRecorderDbContext _context;

protected ReportManagerBase(FlightRecorderDbContext context)
{
_context = context;
}

/// <summary>
/// Generate the report for entity type T given a query that returns rows mapping to that
/// type
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="query"></param>
/// <returns></returns>
protected async Task<IEnumerable<T>> GenerateReport<T>(string query, int pageNumber, int pageSize) where T : class
{
// Pagination using Skip and Take causes the database query to fail with FromSqlRaw, possible
// dependent on the DBS. To avoid this, the results are queried in two steps:
//
// 1) Query the database for all the report results and convert to a list
// 2) Extract the required page from the in-memory list
var all = await _context.Set<T>().FromSqlRaw(query).ToListAsync();
var results = all.Skip((pageNumber - 1) * pageSize).Take(pageSize);
return results;
}

/// <summary>
/// Get the full path to a file held in the SQL sub-folder
/// </summary>
/// <param name="fileName"></param>
/// <returns></returns>
protected static string GetSqlFilePath(string fileName)
{
var assembly = Assembly.GetExecutingAssembly();
var assemblyFolder = Path.GetDirectoryName(assembly.Location);
var sqlFilePath = Path.Combine(assemblyFolder, "sql", fileName);
return sqlFilePath;
}

/// <summary>
/// Read a SQL file and perform placeholder value replacement
/// </summary>
/// <param name="file"></param>
/// <param name="placeHolderValues"></param>
protected static string ReadSqlFile(string file, Dictionary<string, string> placeHolderValues)
{
string content = "";

// Open a stream reader to read the file content
using (var reader = new StreamReader(file))
{
// Read the file content
content = reader.ReadToEnd();

// Perform place holder replacement
content = ReplacePlaceHolders(content, placeHolderValues);
}

return content;
}

/// <summary>
///
/// </summary>
/// <param name="file"></param>
/// <param name="placeHolderValues"></param>
/// <returns></returns>
protected static string ReadSqlResource(string file, Dictionary<string, string> placeHolderValues)
{
string content = "";

// Get the name of the resource and a resource stream for reading it
var assembly = Assembly.GetExecutingAssembly();
var sqlResourceName = $"FlightRecorder.BusinessLogic.Sql.{file}";
var resourceStream = assembly.GetManifestResourceStream(sqlResourceName);

// Open a stream reader to read the file content
using (var reader = new StreamReader(resourceStream))
{
// Read the file content
content = reader.ReadToEnd();

// Perform place holder replacement
content = ReplacePlaceHolders(content, placeHolderValues);
}

return content;
}

/// <summary>
/// Perform place holder replacement on content read from an embedded resource or SQL file
/// </summary>
/// <param name="content"></param>
/// <param name="placeHolderValues"></param>
/// <returns></returns>
private static string ReplacePlaceHolders(string content, Dictionary<string, string> placeHolderValues)
{
foreach (var placeHolder in placeHolderValues.Keys)
{
content = content.Replace(placeHolder, placeHolderValues[placeHolder]);
}

return content;
}
}
}
17 changes: 17 additions & 0 deletions src/FlightRecorder.BusinessLogic/Sql/AirlineStatistics.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
SELECT a.Name,
COUNT( DISTINCT s.Id ) AS "Sightings",
COUNT( DISTINCT f.Id ) AS "Flights",
COUNT( DISTINCT l.Id ) AS "Locations",
COUNT( DISTINCT ai.Id ) AS "Aircraft",
COUNT( DISTINCT m.Id ) AS "Models",
COUNT( DISTINCT ma.Id ) AS "Manufacturers"
FROM AIRLINE a
INNER JOIN FLIGHT f ON f.Airline_Id = a.Id
INNER JOIN SIGHTING s ON s.Flight_Id = f.Id
INNER JOIN LOCATION l ON l.Id = s.Location_Id
INNER JOIN AIRCRAFT ai ON ai.Id = s.Aircraft_Id
INNER JOIN MODEL m ON m.Id = ai.Model_Id
INNER JOIN MANUFACTURER ma ON ma.Id = m.Manufacturer_Id
WHERE s.Date BETWEEN '$from' AND '$to'
GROUP BY a.Name
ORDER BY a.Name ASC;
16 changes: 16 additions & 0 deletions src/FlightRecorder.BusinessLogic/Sql/LocationStatistics.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
SELECT l.Name,
COUNT( DISTINCT s.Id ) AS "Sightings",
COUNT( DISTINCT f.Id ) AS "Flights",
COUNT( DISTINCT ai.Id ) AS "Aircraft",
COUNT( DISTINCT m.Id ) AS "Models",
COUNT( DISTINCT ma.Id ) AS "Manufacturers"
FROM AIRLINE a
INNER JOIN FLIGHT f ON f.Airline_Id = a.Id
INNER JOIN SIGHTING s ON s.Flight_Id = f.Id
INNER JOIN LOCATION l ON l.Id = s.Location_Id
INNER JOIN AIRCRAFT ai ON ai.Id = s.Aircraft_Id
INNER JOIN MODEL m ON m.Id = ai.Model_Id
INNER JOIN MANUFACTURER ma ON ma.Id = m.Manufacturer_Id
WHERE s.Date BETWEEN '$from' AND '$to'
GROUP BY l.Name
ORDER BY l.Name ASC;
16 changes: 16 additions & 0 deletions src/FlightRecorder.BusinessLogic/Sql/ManufacturerStatistics.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
SELECT ma.Name,
COUNT( DISTINCT s.Id ) AS "Sightings",
COUNT( DISTINCT f.Id ) AS "Flights",
COUNT( DISTINCT l.Id ) AS "Locations",
COUNT( DISTINCT ai.Id ) AS "Aircraft",
COUNT( DISTINCT m.Id ) AS "Models"
FROM AIRLINE a
INNER JOIN FLIGHT f ON f.Airline_Id = a.Id
INNER JOIN SIGHTING s ON s.Flight_Id = f.Id
INNER JOIN LOCATION l ON l.Id = s.Location_Id
INNER JOIN AIRCRAFT ai ON ai.Id = s.Aircraft_Id
INNER JOIN MODEL m ON m.Id = ai.Model_Id
INNER JOIN MANUFACTURER ma ON ma.Id = m.Manufacturer_Id
WHERE s.Date BETWEEN '$from' AND '$to'
GROUP BY ma.Name
ORDER BY ma.Name ASC;
Loading

0 comments on commit 7d36f4c

Please sign in to comment.