v1.3.0 Restructure Saxonite workspace and upgrade to .NET 10 (#1)

Move Saxonite’s projects into a dedicated `Workspace` folder and align the solution with .NET 10 and the current Common workspace. Update encryption integration, test project structure and coverage, and repository guidance and upgrade notes.

- Relocate application and library projects while retaining Git rename history.
- Split tests into Admin, Online, and Shared projects; add representative behavioral and serialization tests.
- Update Common integration, application version, and repository documentation.

Application version: 1.3.0

Reviewed-on: #1
Co-authored-by: andy <andy@sarand.com>
This commit was merged in pull request #1.
This commit is contained in:
2026-10-16 18:24:16 +01:00
committed by andy
parent 2c3fb146ab
commit 9f22b522df
1996 changed files with 1992 additions and 1179 deletions
@@ -0,0 +1,41 @@
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Saxonite.Net.Core.Web.Admin.App
{
//=======================================================================
public class Program
{
//-------------------------------------------------------------------
public static void Main(string[] args)
{
try
{
CreateHostBuilder(args).Build().Run();
}
finally
{
// Cleanup - globals, async processes etc.
if (SaxApplication_Admin.AppInstance != null)
SaxApplication_Admin.AppInstance.Dispose();
}
}
//-------------------------------------------------------------------
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<SaxStartup_Admin>();
});
//-------------------------------------------------------------------
}
//=======================================================================
}
@@ -0,0 +1,245 @@
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Sarand.Net.Standard.Base.App;
using Sarand.Net.Standard.Base.Bus;
using Sarand.Net.Standard.Base.Core;
using Sarand.Net.Standard.Base.Dbas;
using Sarand.Net.Standard.Base.Email;
using Sarand.Net.Standard.Base.IO;
using Sarand.Net.Standard.Base.Utils;
using Sarand.Net.Standard.Dbas.Base;
using Saxonite.Net.Core.Web.Admin.Consoles;
using Saxonite.Net.Core.Web.Base.App;
using Saxonite.Net.Core.Web.Base.Helpers;
using Saxonite.Net.Standard.Base.App;
using Saxonite.Net.Standard.Base.Bus;
using Saxonite.Net.Standard.Base.Consoles;
using Saxonite.Net.Standard.Base.Helpers;
using Saxonite.Net.Standard.Base.Mgr;
using Saxonite.Net.Standard.Dbas.App;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace Saxonite.Net.Core.Web.Admin.App
{
//=======================================================================
public class SaxApplication_Admin : SaxApplication_Web
{
//-------------------------------------------------------------------
// Data members.
private bool generateDatabaseDefinitions = true;
private bool populateRegions = false;
private Dictionary<String, SaxAppConfig> appConfigs = new Dictionary<string, SaxAppConfig>();
private Dictionary<String, SaxEmailConfig> emailConfigs = new Dictionary<string, SaxEmailConfig>();
// Session contexts.
private class SessionContextData_Admin : SessionContextData_Web
{
public SessionContextData_Admin(SaxSessionContext_Web sessionContext) : base(sessionContext) { }
}
private readonly Dictionary<int, SessionContextData_Admin> sessionContexts = new Dictionary<int, SessionContextData_Admin>();
//-------------------------------------------------------------------
// Construction.
public SaxApplication_Admin(IConfiguration configuration) : base(Sax.Application.Admin)
{
InitialiseTemporaryLoggingFolder();
try
{
AppInstance = this;
Configuration = configuration;
ConsoleHelper_Admin = new SaxConsoleHelper_Admin(this);
}
catch (Exception exc)
{
SaLogger.Error($"{GetType().Name}.CTR {ExceptionUtil.GetErrorStateMessage(exc)}");
throw;
}
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
// Clear globals.
AppInstance = null;
// Console helper.
if (ConsoleHelper_Admin != null)
ConsoleHelper_Admin.Dispose();
ConsoleHelper_Admin = null;
}
base.Dispose(disposing);
}
//-------------------------------------------------------------------
// Properties.
public static SaxApplication_Admin AppInstance { get; private set; }
public override SaxConsoleHelper ConsoleHelper { get { return ConsoleHelper_Admin; } }
public SaxConsoleHelper_Admin ConsoleHelper_Admin { get; private set; }
// Database.
public override ISacsDatabase GetDatabase(SaxSessionContext sessionContext, Sax.Database databaseType)
{
return sessionContexts.GetValue(sessionContext.Index, null)?.Databases.GetValue(databaseType, null);
}
// Configuration.
public override SaxAppConfig AppConfig { get { return appConfigs.Values.ToList().getConfigForApplication(); } }
public override SaxEmailConfig EmailConfig { get { return emailConfigs.Values.ToList().getConfigForSession(AppConfig); } }
public override String SessionCookieName { get { return Sax.Config.Admin.Session.Cookie.Name; } }
// Startup messages - for debugging. See Admin.Views._Layout.
public List<String> Messages { get; } = new List<String>();
//-------------------------------------------------------------------
// Initialisation.
/// <summary>Initialise environment - from Startup.</summary>
internal void InitWebHostEnvironment(IWebHostEnvironment env)
{
InitEnvironment(env);
}
/// <summary>Initialise environment - from internal (above) or unit tests.</summary>
protected void InitEnvironment(IWebHostEnvironment env)
{
base.InitEnvironment(env, "Sax-Web-Adm");
try
{
// Application configuration.
appConfigs = SaxAppConfigHelper.ExtractAppConfigs(SaxConfiguration.GetSection("AppConfigs"));
emailConfigs = SaxEmailConfigHelper.ExtractEmailConfigs(SaxConfiguration.GetSection("EmailConfigs"));
// Ensure root log folder ends with 'Admin' folder.
LoggingRootFolder = AppConfig.Logging.RootFolder;
if (!StringUtil.IsBlank(LoggingRootFolder) && !Regex.IsMatch(LoggingRootFolder, "[\\\\/]Admin$", RegexOptions.IgnoreCase))
LoggingRootFolder = StringUtil.CombineFilePath(LoggingRootFolder, "Admin");
// Application instance.
if (AppConfig.Instance.EqualsCI("Dev")) instanceType = Sax.InstanceType.Dev;
else if (AppConfig.Instance.ContainsCI("Q")) instanceType = Sax.InstanceType.QC;
else if (AppConfig.Instance.ContainsCI("UAT")) instanceType = Sax.InstanceType.UAT;
else if (AppConfig.Instance.EqualsCI("PreProd")) instanceType = Sax.InstanceType.UAT;
// Initialise hard-coded logging path.
if (StringUtil.IsEmpty(LoggingRootFolder))
{
if (SaxSystem.ComputerName.EqualsCI("WIN-BPCLM1QUOMT"))
LoggingRootFolder = @"D:\Saxonite\Logs\" + (Development ? "Dev" : "Test");
}
if (DirectoryUtil.DirectoryExists(LoggingRootFolder))
SaLogger.LogFolder = LoggingRootFolder;
LogApplicationStart(SysUtil.Fn);
//Messages.Add($"Database location: {AppConfig.CoreDatabase.Location}");
//Messages.Add($"LoggingRootFolder: {SaLogger.LogFolder}");
//Messages.Add($"OdbcDrivers: {OdbcUtil.GetOdbcDriverNames().Concat(", ")}");
//Messages.Add($"DbConnection: {coreDatabase.ConnectionString}");
// Session context - only one context available/necessary (for now).
SessionContextData_Admin sessionContextData = new SessionContextData_Admin(new SaxSessionContext_Web(100, ApplicationType));
ConfigureDatabaseConnection(sessionContextData, Sax.Database.Core);
ConfigureDatabaseConnection(sessionContextData, Sax.Database.Reference);
ConfigureDatabaseConnection(sessionContextData, Sax.Database.Support);
sessionContexts.Add(sessionContextData.SessionContext.Index, sessionContextData);
SessionContext = sessionContextData.SessionContext;
}
catch (Exception exc)
{
SaLogger.Error(SysUtil.Fn + " " + ExceptionUtil.GetLogMessage(exc));
}
// Occasional startup processing.
if (generateDatabaseDefinitions)
GenerateDatabaseDefinitions(SessionContext);
if (populateRegions)
populateRegionsFromDataFile(SessionContext);
}
//-------------------------------------------------------------------
private void GenerateDatabaseDefinitions(SaxSessionContext sessionContext)
{
// Generate database definitions.
if (!generateDatabaseDefinitions || !SaxSystem.DebugMode || SessionContext == null) return;
if (!StringUtil.EmptyOnNull(EnvironmentName).EqualsCI("Development")) return;
// Core database.
ISacsDatabase coreDatabase = GetDatabase(sessionContext, Sax.Database.Core);
if (coreDatabase != null && !StringUtil.IsEmpty(coreDatabase.ConnectionString))
{
SaxDbDfnGenerator.Generate(Sax.Database.Core, coreDatabase.ConnectionString);
//SaUpdateLogDbDfnGenerator.Generate(database.ConnectionString);
}
// Reference database.
ISacsDatabase referenceDatabase = GetDatabase(sessionContext, Sax.Database.Reference);
if (referenceDatabase != null && !StringUtil.IsEmpty(referenceDatabase.ConnectionString))
SaxDbDfnGenerator.Generate(Sax.Database.Reference, referenceDatabase.ConnectionString);
// Support database.
ISacsDatabase supportDatabase = GetDatabase(sessionContext, Sax.Database.Support);
if (supportDatabase != null && !StringUtil.IsEmpty(supportDatabase.ConnectionString))
SaxDbDfnGenerator.Generate(Sax.Database.Support, supportDatabase.ConnectionString);
}
//-------------------------------------------------------------------
private void populateRegionsFromDataFile(SaxSessionContext sessionContext)
{
using (SaxInternals internals = new SaxInternals(sessionContext))
{
SaxReferenceMgr referenceMgr = internals.ReferenceMgr;
String regionsData = SaxResourceHelper.GetResourceText(SaxResourceHelper.TextResource.Data_CountriesWithRegions_Txt);
String[] regionDataValues = regionsData.Split("\n");
SysId parentRegionId = null;
for (int idx = 0; idx < regionDataValues.Length; idx++)
{
String regionData = regionDataValues[idx].ReplaceAll("\r", "");
if (StringUtil.IsEmpty(regionData)) continue;
if (regionData.Length > 3 && regionData[2] == ':')
{
String iso2Code = regionData.Substring(0, 2);
String name = regionData.Substring(3);
SaxRegion region = getRegionByName(referenceMgr, name);
if (region == null)
{
region = referenceMgr.CreateRegion();
region.RegionType = Sax.RegionType.Country;
region.CountryIso2Code = iso2Code;
region.Name = name;
region.RegionIdParent = parentRegionId;
if (!referenceMgr.InsertRegion(region, new DbUpdateInfo(new SysId(), "", "")))
{
SaLogger.Error("Failed to insert region - " + referenceMgr.ErrorStateMessage);
return;
}
}
}
else
{
// Sub continent.
SaxRegion region = getRegionByName(referenceMgr, regionData);
if (region == null)
{
region = referenceMgr.CreateRegion();
region.RegionType = Sax.RegionType.SubContinent;
region.Name = regionData;
if (!referenceMgr.InsertRegion(region, new DbUpdateInfo(new SysId(), "", "")))
{
SaLogger.Error("Failed to insert region - " + referenceMgr.ErrorStateMessage);
return;
}
}
parentRegionId = region.Id;
}
}
}
}
private SaxRegion getRegionByName(SaxReferenceMgr referenceMgr, String name)
{
SaxRegion.Params prmRegion = new SaxRegion.Params();
prmRegion.NameFull = name;
return referenceMgr.GetRegion(prmRegion);
}
//-------------------------------------------------------------------
}
//=======================================================================
}
@@ -0,0 +1,47 @@
using Microsoft.Extensions.Localization;
using Saxonite.Net.Core.Web.Base.App;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Saxonite.Net.Core.Web
{
// <summary>
/// Dummy classes to group shared resources - named as per '.resx' file in root namespace.
/// <para>Required to create the IStringLocalizer instance using the type from the class.</para>
/// <para>Name of the class is used as the prefix for localisation filenames.</para>
/// <para>Resource files are in the Localization ResourcesPath in Startup.cs.</para>
/// </summary>
public class SaxAdminProperties { }
// Class must be in Root/Default Namespace to reference properties with same name.
// NB 'Assembly name' must be the same as 'Default Namespace' until 'Root Namespace' can be set (.Net Core 2.1?).
// Ref. Project Properties - Application.
}
namespace Saxonite.Net.Core.Web.Admin.App
{
//=======================================================================
public class SaxLocaliser_Admin : SaxLocaliser
{
//-------------------------------------------------------------------
// Data members.
private readonly IStringLocalizer<SaxAdminProperties> adminProperties;
//-------------------------------------------------------------------
/// <summary>
/// Localisers are supplied using construction injection.
/// </summary>
public SaxLocaliser_Admin(IStringLocalizer<SaxAdminProperties> webAppProperties)
{
this.adminProperties = webAppProperties;
}
//-------------------------------------------------------------------
// Properties.
public override IStringLocalizer MainProperties { get { return adminProperties; } }
//-------------------------------------------------------------------
}
//=======================================================================
}
@@ -0,0 +1,91 @@
using Newtonsoft.Json;
using Sarand.Net.Standard.Base.Bus;
using Sarand.Net.Standard.Base.Helpers;
using Saxonite.Net.Core.Web.Admin.Controllers;
using Saxonite.Net.Core.Web.Base.App;
using Saxonite.Net.Standard.Base.App;
using Saxonite.Net.Standard.Base.Bus;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Saxonite.Net.Core.Web.Admin.App
{
//=======================================================================
public class SaxSessionData_Admin : SaxSessionData_Web
{
//-------------------------------------------------------------------
// Construction.
[JsonConstructor]
private SaxSessionData_Admin(long sessionIdx, int itemIdx, SaxSessionContext sessionContext) :
base(sessionIdx, itemIdx, sessionContext)
{ }
/// <summary>
/// SessionData construction.
/// <para>SessionContext and UserAccount must be supplied.</para>
/// <para>Organisation is optional.</para>
/// </summary>
public SaxSessionData_Admin(SaxSessionContext sessionContext, SaxUserAccount userAccount, List<SaxUserRole> userRoles) :
base(sessionContext, userAccount, userRoles)
{ }
//-------------------------------------------------------------------
// Properties.
//-------------------------------------------------------------------
// User details.
private class UserData_Admin : UserData
{
public UserData_Admin(SaxSessionContext sessionContext, SaxUserAccount userAccount, List<SaxUserRole> userRoles) :
base(sessionContext, userAccount, userRoles)
{ }
public UserData_Admin(UserData userData) :
base(userData)
{ }
}
protected override UserData CreateUserData(SaxSessionContext sessionContext, SaxUserAccount userAccount, List<SaxUserRole> userRoles) { return new UserData_Admin(sessionContext, userAccount, userRoles); }
/// <summary>User - JSON deserialisation route.</summary>
public new UserData User { get { return base.User; } set { SetUser(new UserData_Admin(value)); } }
//-------------------------------------------------------------------
// General filters.
public class FilterData
{
// Sport.
public SysId SportId { get; set; }
// Region.
public SysId RegionId { get; set; }
}
public FilterData Filter { get; private set; } = new FilterData();
//-------------------------------------------------------------------
// System.
public class SystemData
{
// Databases.
public class DatabaseData
{
}
public DatabaseData Database { get; private set; } = new DatabaseData();
// Logs.
public class LogsData
{
public class FilterData
{
public String Folder { get; set; }
public String Year { get; set; }
public String Month { get; set; }
}
public FilterData Filter { get; private set; } = new FilterData();
}
public LogsData Logs { get; private set; } = new LogsData();
}
public SystemData System { get; private set; } = new SystemData();
//-------------------------------------------------------------------
//-------------------------------------------------------------------
}
//=======================================================================
}
@@ -0,0 +1,75 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Saxonite.Net.Core.Web.Admin.Consoles;
using Saxonite.Net.Core.Web.Base.App;
using Saxonite.Net.Standard.Base.App;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Saxonite.Net.Core.Web.Admin.App
{
//=======================================================================
public class SaxStartup_Admin : SaxStartup
{
//-------------------------------------------------------------------
// Data members.
private SaxApplication_Admin application = null;
//-------------------------------------------------------------------
// Construction.
public SaxStartup_Admin(IConfiguration configuration) : base(configuration)
{
// Initialise application.
application = new SaxApplication_Admin(Configuration);
}
//-------------------------------------------------------------------
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
// Common configuration.
ConfigureServicesConfig configureServicesConfig = new ConfigureServicesConfig();
configureServicesConfig.services = services;
configureServicesConfig.sessionCookieName = Sax.Config.Admin.Session.Cookie.Name;
configureServicesConfig.sessionCookieHttpOnly = Sax.Config.Admin.Session.Cookie.HttpOnly;
configureServicesConfig.sessionIdleTimeout = Sax.Config.Admin.Session.Cookie.IdleTimeout;
ConfigureServices(configureServicesConfig);
// Application localisation.
services.AddSingleton<SaxLocaliser, SaxLocaliser_Admin>();
services.Configure<CookiePolicyOptions>(options =>
{
// This lambda determines whether user consent for non-essential cookies is needed for a given request.
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = Microsoft.AspNetCore.Http.SameSiteMode.None;
});
// Hosted (schedule) services.
services.AddHostedService<SaxHostedServiceConsole_Admin>();
}
//-------------------------------------------------------------------
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostApplicationLifetime hostApplicationLifetime, IWebHostEnvironment env)
{
application.InitWebHostEnvironment(env);
// Common configuration.
ConfigureConfig configureConfig = new ConfigureConfig();
configureConfig.app = app;
configureConfig.hostApplicationLifetime = hostApplicationLifetime;
configureConfig.env = env;
Configure(configureConfig);
}
//-------------------------------------------------------------------
}
//=======================================================================
}