diff --git a/src/PepperDash.Essentials.Core/Web/RequestHandlers/GetPackageManifestRequestHandler.cs b/src/PepperDash.Essentials.Core/Web/RequestHandlers/GetPackageManifestRequestHandler.cs
index 3d1b9eaa..284f3e2d 100644
--- a/src/PepperDash.Essentials.Core/Web/RequestHandlers/GetPackageManifestRequestHandler.cs
+++ b/src/PepperDash.Essentials.Core/Web/RequestHandlers/GetPackageManifestRequestHandler.cs
@@ -6,254 +6,253 @@ using Newtonsoft.Json;
using PepperDash.Core.Web.RequestHandlers;
using PepperDash.Essentials.Core.Config;
-namespace PepperDash.Essentials.Core.Web.RequestHandlers
+namespace PepperDash.Essentials.Core.Web.RequestHandlers;
+
+///
+/// Represents a GetPackageManifestRequestHandler
+///
+public class GetPackageManifestRequestHandler : WebApiBaseRequestHandler
{
///
- /// Represents a GetPackageManifestRequestHandler
+ /// Constructor
///
- public class GetPackageManifestRequestHandler : WebApiBaseRequestHandler
+ ///
+ /// base(true) enables CORS support by default
+ ///
+ public GetPackageManifestRequestHandler()
+ : base(true)
{
- ///
- /// Constructor
- ///
- ///
- /// base(true) enables CORS support by default
- ///
- public GetPackageManifestRequestHandler()
- : base(true)
+ }
+
+ ///
+ /// Handles GET method requests
+ ///
+ ///
+ protected override void HandleGet(HttpCwsContext context)
+ {
+ try
{
+ var result = CloneVersionData(ConfigReader.ConfigObject?.Versions) ?? new VersionData();
+
+ PopulateEssentials(result);
+ PopulatePackages(result);
+
+ var js = JsonConvert.SerializeObject(result, Formatting.Indented);
+
+ context.Response.StatusCode = 200;
+ context.Response.StatusDescription = "OK";
+ context.Response.ContentType = "application/json";
+ context.Response.ContentEncoding = System.Text.Encoding.UTF8;
+ context.Response.Write(js, false);
+ context.Response.End();
}
-
- ///
- /// Handles GET method requests
- ///
- ///
- protected override void HandleGet(HttpCwsContext context)
+ catch (Exception ex)
{
- try
- {
- var result = CloneVersionData(ConfigReader.ConfigObject?.Versions) ?? new VersionData();
-
- PopulateEssentials(result);
- PopulatePackages(result);
-
- var js = JsonConvert.SerializeObject(result, Formatting.Indented);
-
- context.Response.StatusCode = 200;
- context.Response.StatusDescription = "OK";
- context.Response.ContentType = "application/json";
- context.Response.ContentEncoding = System.Text.Encoding.UTF8;
- context.Response.Write(js, false);
- context.Response.End();
- }
- catch (Exception ex)
- {
- PepperDash.Core.Debug.LogMessage(ex, "Exception handling GET /packageManifest request");
- context.Response.StatusCode = 500;
- context.Response.StatusDescription = "Internal Server Error";
- context.Response.End();
- }
- }
-
- ///
- /// Deep-copies the config's VersionData so the live config object is never mutated
- ///
- private static VersionData CloneVersionData(VersionData source)
- {
- if (source == null)
- {
- return null;
- }
-
- var json = JsonConvert.SerializeObject(source);
- return JsonConvert.DeserializeObject(json);
- }
-
- ///
- /// Enriches (or creates) the essentials entry from the loaded PepperDash.Essentials.Core assembly
- ///
- private static void PopulateEssentials(VersionData result)
- {
- var essentials = result.Essentials ?? new PackageVersion();
-
- essentials.Version = Global.AssemblyVersion;
-
- // The main program assembly (PackageId "PepperDashEssentials") is what's actually published
- // to NuGet, but this handler lives in PepperDash.Essentials.Core, which can't reference that
- // project's types directly (Essentials -> Core, not the reverse). PluginLoader.EssentialsAssembly.Assembly
- // is unreliable (often left null - see PluginLoader.SetEssentialsAssembly), so look it up
- // directly from the loaded AppDomain by its Directory.Build.props-embedded PackageId metadata
- // (every project's .csproj sets its own PackageId explicitly), falling back to this handler's
- // own (Core) assembly if it can't be found.
- var essentialsAssembly = AppDomain.CurrentDomain.GetAssemblies()
- .FirstOrDefault(a => string.Equals(GetAssemblyMetadataValue(a, "PackageId"), "PepperDashEssentials", StringComparison.OrdinalIgnoreCase))
- ?? typeof(GetPackageManifestRequestHandler).Assembly;
-
- var repoUrl = TrimTrailingGit(GetAssemblyMetadataValue(essentialsAssembly, "RepositoryUrl"));
- if (!string.IsNullOrEmpty(repoUrl))
- {
- essentials.RepoUrl = repoUrl;
- }
-
- var name = GetAssemblyProduct(essentialsAssembly);
- if (!string.IsNullOrEmpty(name))
- {
- essentials.Name = name;
- }
-
- // Prefer the PackageId embedded via Directory.Build.props' AssemblyMetadata item (the
- // authoritative source, matching the actual published PackageId) over any config-supplied
- // or hardcoded value.
- var reflectedPackageId = GetAssemblyMetadataValue(essentialsAssembly, "PackageId");
- if (!string.IsNullOrEmpty(reflectedPackageId))
- {
- essentials.PackageId = reflectedPackageId;
- }
- else if (string.IsNullOrEmpty(essentials.PackageId))
- {
- essentials.PackageId = "PepperDashEssentials";
- }
-
- result.Essentials = essentials;
- }
-
- ///
- /// Merges reflection data from loaded plugin assemblies with the config's packages list
- ///
- private static void PopulatePackages(VersionData result)
- {
- // Filter out null entries defensively - the packages list is deserialized from user-editable
- // config JSON, so a malformed "packages": [null, ...] shouldn't throw and 500 the endpoint.
- var configPackages = (result.Packages ?? new System.Collections.Generic.List())
- .Where(p => p != null)
- .ToList();
- var matchedConfigPackages = new System.Collections.Generic.HashSet();
- var mergedPackages = new System.Collections.Generic.List();
-
- foreach (var loaded in PluginLoader.EssentialsPluginAssemblies.Where(a => a.Assembly != null))
- {
- var reflectedVersion = loaded.Version;
- if (string.IsNullOrEmpty(reflectedVersion))
- {
- // Never emit an entry with no version - the extension's parser drops entries
- // whose version isn't a string.
- continue;
- }
-
- var reflectedRepoUrl = TrimTrailingGit(GetAssemblyMetadataValue(loaded.Assembly, "RepositoryUrl"));
- var reflectedName = GetAssemblyProduct(loaded.Assembly);
-
- // Plugins built from a Directory.Build.props that embeds
- // carry their PackageId
- // directly - this is authoritative and should be preferred over the title/name fallback chain.
- var reflectedPackageId = GetAssemblyMetadataValue(loaded.Assembly, "PackageId");
-
- var assemblyTitle = GetAssemblyTitle(loaded.Assembly);
- var assemblyName = loaded.Assembly.GetName().Name;
- var assemblyNameNoSeriesSuffix = StripTrailingSeriesSuffix(assemblyName);
-
- var match = configPackages.FirstOrDefault(p =>
- !matchedConfigPackages.Contains(p) &&
- !string.IsNullOrEmpty(p.PackageId) &&
- (string.Equals(p.PackageId, reflectedPackageId, StringComparison.OrdinalIgnoreCase) ||
- string.Equals(p.PackageId, assemblyTitle, StringComparison.OrdinalIgnoreCase) ||
- string.Equals(p.PackageId, assemblyName, StringComparison.OrdinalIgnoreCase) ||
- string.Equals(p.PackageId, assemblyNameNoSeriesSuffix, StringComparison.OrdinalIgnoreCase)));
-
- if (match != null)
- {
- matchedConfigPackages.Add(match);
-
- mergedPackages.Add(new PackageVersion
- {
- Name = !string.IsNullOrEmpty(match.Name) ? match.Name : reflectedName,
- RepoUrl = !string.IsNullOrEmpty(match.RepoUrl) ? match.RepoUrl : reflectedRepoUrl,
- PackageId = !string.IsNullOrEmpty(reflectedPackageId) ? reflectedPackageId : match.PackageId,
- Version = reflectedVersion
- });
- }
- else
- {
- // Loaded but not present (or not matched) in config - emit the reflected PackageId
- // when the assembly carries one, otherwise leave it null as before.
- mergedPackages.Add(new PackageVersion
- {
- Name = reflectedName,
- RepoUrl = reflectedRepoUrl,
- PackageId = reflectedPackageId,
- Version = reflectedVersion,
- });
- }
- }
-
- // Configured but not currently loaded - pass through unchanged
- mergedPackages.AddRange(configPackages.Where(p => !matchedConfigPackages.Contains(p)));
-
- result.Packages = mergedPackages;
- }
-
- private static string GetAssemblyMetadataValue(Assembly assembly, string key)
- {
- if (assembly == null)
- {
- return null;
- }
-
- var match = assembly.GetCustomAttributes(typeof(AssemblyMetadataAttribute), false)
- .Cast()
- .FirstOrDefault(a => string.Equals(a.Key, key, StringComparison.OrdinalIgnoreCase));
-
- return match?.Value;
- }
-
- private static string GetAssemblyProduct(Assembly assembly)
- {
- if (assembly == null)
- {
- return null;
- }
-
- var attribute = assembly.GetCustomAttributes(typeof(AssemblyProductAttribute), false)
- .FirstOrDefault() as AssemblyProductAttribute;
-
- return attribute?.Product;
- }
-
- private static string GetAssemblyTitle(Assembly assembly)
- {
- if (assembly == null)
- {
- return null;
- }
-
- var attribute = assembly.GetCustomAttributes(typeof(AssemblyTitleAttribute), false)
- .FirstOrDefault() as AssemblyTitleAttribute;
-
- return attribute?.Title;
- }
-
- private static string StripTrailingSeriesSuffix(string assemblyName)
- {
- const string suffix = ".4Series";
-
- if (string.IsNullOrEmpty(assemblyName) || !assemblyName.EndsWith(suffix, StringComparison.OrdinalIgnoreCase))
- {
- return assemblyName;
- }
-
- return assemblyName.Substring(0, assemblyName.Length - suffix.Length);
- }
-
- private static string TrimTrailingGit(string repoUrl)
- {
- const string suffix = ".git";
-
- if (string.IsNullOrEmpty(repoUrl) || !repoUrl.EndsWith(suffix, StringComparison.OrdinalIgnoreCase))
- {
- return repoUrl;
- }
-
- return repoUrl.Substring(0, repoUrl.Length - suffix.Length);
+ PepperDash.Core.Debug.LogMessage(ex, "Exception handling GET /packageManifest request");
+ context.Response.StatusCode = 500;
+ context.Response.StatusDescription = "Internal Server Error";
+ context.Response.End();
}
}
+
+ ///
+ /// Deep-copies the config's VersionData so the live config object is never mutated
+ ///
+ private static VersionData CloneVersionData(VersionData source)
+ {
+ if (source == null)
+ {
+ return null;
+ }
+
+ var json = JsonConvert.SerializeObject(source);
+ return JsonConvert.DeserializeObject(json);
+ }
+
+ ///
+ /// Enriches (or creates) the essentials entry from the loaded PepperDash.Essentials.Core assembly
+ ///
+ private static void PopulateEssentials(VersionData result)
+ {
+ var essentials = result.Essentials ?? new PackageVersion();
+
+ essentials.Version = Global.AssemblyVersion;
+
+ // The main program assembly (PackageId "PepperDashEssentials") is what's actually published
+ // to NuGet, but this handler lives in PepperDash.Essentials.Core, which can't reference that
+ // project's types directly (Essentials -> Core, not the reverse). PluginLoader.EssentialsAssembly.Assembly
+ // is unreliable (often left null - see PluginLoader.SetEssentialsAssembly), so look it up
+ // directly from the loaded AppDomain by its Directory.Build.props-embedded PackageId metadata
+ // (every project's .csproj sets its own PackageId explicitly), falling back to this handler's
+ // own (Core) assembly if it can't be found.
+ var essentialsAssembly = AppDomain.CurrentDomain.GetAssemblies()
+ .FirstOrDefault(a => string.Equals(GetAssemblyMetadataValue(a, "PackageId"), "PepperDashEssentials", StringComparison.OrdinalIgnoreCase))
+ ?? typeof(GetPackageManifestRequestHandler).Assembly;
+
+ var repoUrl = TrimTrailingGit(GetAssemblyMetadataValue(essentialsAssembly, "RepositoryUrl"));
+ if (!string.IsNullOrEmpty(repoUrl))
+ {
+ essentials.RepoUrl = repoUrl;
+ }
+
+ var name = GetAssemblyProduct(essentialsAssembly);
+ if (!string.IsNullOrEmpty(name))
+ {
+ essentials.Name = name;
+ }
+
+ // Prefer the PackageId embedded via Directory.Build.props' AssemblyMetadata item (the
+ // authoritative source, matching the actual published PackageId) over any config-supplied
+ // or hardcoded value.
+ var reflectedPackageId = GetAssemblyMetadataValue(essentialsAssembly, "PackageId");
+ if (!string.IsNullOrEmpty(reflectedPackageId))
+ {
+ essentials.PackageId = reflectedPackageId;
+ }
+ else if (string.IsNullOrEmpty(essentials.PackageId))
+ {
+ essentials.PackageId = "PepperDashEssentials";
+ }
+
+ result.Essentials = essentials;
+ }
+
+ ///
+ /// Merges reflection data from loaded plugin assemblies with the config's packages list
+ ///
+ private static void PopulatePackages(VersionData result)
+ {
+ // Filter out null entries defensively - the packages list is deserialized from user-editable
+ // config JSON, so a malformed "packages": [null, ...] shouldn't throw and 500 the endpoint.
+ var configPackages = (result.Packages ?? new System.Collections.Generic.List())
+ .Where(p => p != null)
+ .ToList();
+ var matchedConfigPackages = new System.Collections.Generic.HashSet();
+ var mergedPackages = new System.Collections.Generic.List();
+
+ foreach (var loaded in PluginLoader.EssentialsPluginAssemblies.Where(a => a.Assembly != null))
+ {
+ var reflectedVersion = loaded.Version;
+ if (string.IsNullOrEmpty(reflectedVersion))
+ {
+ // Never emit an entry with no version - the extension's parser drops entries
+ // whose version isn't a string.
+ continue;
+ }
+
+ var reflectedRepoUrl = TrimTrailingGit(GetAssemblyMetadataValue(loaded.Assembly, "RepositoryUrl"));
+ var reflectedName = GetAssemblyProduct(loaded.Assembly);
+
+ // Plugins built from a Directory.Build.props that embeds
+ // carry their PackageId
+ // directly - this is authoritative and should be preferred over the title/name fallback chain.
+ var reflectedPackageId = GetAssemblyMetadataValue(loaded.Assembly, "PackageId");
+
+ var assemblyTitle = GetAssemblyTitle(loaded.Assembly);
+ var assemblyName = loaded.Assembly.GetName().Name;
+ var assemblyNameNoSeriesSuffix = StripTrailingSeriesSuffix(assemblyName);
+
+ var match = configPackages.FirstOrDefault(p =>
+ !matchedConfigPackages.Contains(p) &&
+ !string.IsNullOrEmpty(p.PackageId) &&
+ (string.Equals(p.PackageId, reflectedPackageId, StringComparison.OrdinalIgnoreCase) ||
+ string.Equals(p.PackageId, assemblyTitle, StringComparison.OrdinalIgnoreCase) ||
+ string.Equals(p.PackageId, assemblyName, StringComparison.OrdinalIgnoreCase) ||
+ string.Equals(p.PackageId, assemblyNameNoSeriesSuffix, StringComparison.OrdinalIgnoreCase)));
+
+ if (match != null)
+ {
+ matchedConfigPackages.Add(match);
+
+ mergedPackages.Add(new PackageVersion
+ {
+ Name = !string.IsNullOrEmpty(match.Name) ? match.Name : reflectedName,
+ RepoUrl = !string.IsNullOrEmpty(match.RepoUrl) ? match.RepoUrl : reflectedRepoUrl,
+ PackageId = !string.IsNullOrEmpty(reflectedPackageId) ? reflectedPackageId : match.PackageId,
+ Version = reflectedVersion
+ });
+ }
+ else
+ {
+ // Loaded but not present (or not matched) in config - emit the reflected PackageId
+ // when the assembly carries one, otherwise leave it null as before.
+ mergedPackages.Add(new PackageVersion
+ {
+ Name = reflectedName,
+ RepoUrl = reflectedRepoUrl,
+ PackageId = reflectedPackageId,
+ Version = reflectedVersion,
+ });
+ }
+ }
+
+ // Configured but not currently loaded - pass through unchanged
+ mergedPackages.AddRange(configPackages.Where(p => !matchedConfigPackages.Contains(p)));
+
+ result.Packages = mergedPackages;
+ }
+
+ private static string GetAssemblyMetadataValue(Assembly assembly, string key)
+ {
+ if (assembly == null)
+ {
+ return null;
+ }
+
+ var match = assembly.GetCustomAttributes(typeof(AssemblyMetadataAttribute), false)
+ .Cast()
+ .FirstOrDefault(a => string.Equals(a.Key, key, StringComparison.OrdinalIgnoreCase));
+
+ return match?.Value;
+ }
+
+ private static string GetAssemblyProduct(Assembly assembly)
+ {
+ if (assembly == null)
+ {
+ return null;
+ }
+
+ var attribute = assembly.GetCustomAttributes(typeof(AssemblyProductAttribute), false)
+ .FirstOrDefault() as AssemblyProductAttribute;
+
+ return attribute?.Product;
+ }
+
+ private static string GetAssemblyTitle(Assembly assembly)
+ {
+ if (assembly == null)
+ {
+ return null;
+ }
+
+ var attribute = assembly.GetCustomAttributes(typeof(AssemblyTitleAttribute), false)
+ .FirstOrDefault() as AssemblyTitleAttribute;
+
+ return attribute?.Title;
+ }
+
+ private static string StripTrailingSeriesSuffix(string assemblyName)
+ {
+ const string suffix = ".4Series";
+
+ if (string.IsNullOrEmpty(assemblyName) || !assemblyName.EndsWith(suffix, StringComparison.OrdinalIgnoreCase))
+ {
+ return assemblyName;
+ }
+
+ return assemblyName.Substring(0, assemblyName.Length - suffix.Length);
+ }
+
+ private static string TrimTrailingGit(string repoUrl)
+ {
+ const string suffix = ".git";
+
+ if (string.IsNullOrEmpty(repoUrl) || !repoUrl.EndsWith(suffix, StringComparison.OrdinalIgnoreCase))
+ {
+ return repoUrl;
+ }
+
+ return repoUrl.Substring(0, repoUrl.Length - suffix.Length);
+ }
}