diff --git a/src/ServiceControl.Persistence/PersistenceManifest.cs b/src/ServiceControl.Persistence/PersistenceManifest.cs index 1c1c6dff73..7f5f0a1d67 100644 --- a/src/ServiceControl.Persistence/PersistenceManifest.cs +++ b/src/ServiceControl.Persistence/PersistenceManifest.cs @@ -18,9 +18,10 @@ public class PersistenceManifest public required string Description { get; set; } - public required string AssemblyName { get; set; } + // Absent on manifests for persisters that no longer ship an assembly, such as RavenDB 3.5 + public string? AssemblyName { get; set; } - public required string TypeName { get; set; } + public string? TypeName { get; set; } public bool IsSupported { get; set; } = true; @@ -59,10 +60,7 @@ static PersistenceManifestLibrary() try { - foreach (var manifestFile in Directory.EnumerateFiles(assemblyDirectory, "persistence.manifest", SearchOption.AllDirectories)) - { - PersistenceManifests.Add(DeserializeManifest(manifestFile)); - } + PersistenceManifests.AddRange(LoadManifests(Directory.EnumerateFiles(assemblyDirectory, "persistence.manifest", SearchOption.AllDirectories))); } catch (Exception ex) { @@ -71,10 +69,7 @@ static PersistenceManifestLibrary() try { - foreach (var manifestFile in DevelopmentPersistenceLocations.ManifestFiles) - { - PersistenceManifests.Add(DeserializeManifest(manifestFile)); - } + PersistenceManifests.AddRange(LoadManifests(DevelopmentPersistenceLocations.ManifestFiles)); } catch (Exception ex) { @@ -84,6 +79,26 @@ static PersistenceManifestLibrary() PersistenceManifests.ForEach(m => logger.LogInformation("Found persistence manifest for {ManifestDisplayName}", m.DisplayName)); } + // One unreadable manifest must not hide the persisters enumerated after it + internal static List LoadManifests(IEnumerable manifestFiles) + { + var manifests = new List(); + + foreach (var manifestFile in manifestFiles) + { + try + { + manifests.Add(DeserializeManifest(manifestFile)); + } + catch (Exception ex) + { + logger.LogWarning(ex, "Failed to load persistence manifest {ManifestFile}", manifestFile); + } + } + + return manifests; + } + static PersistenceManifest DeserializeManifest(string manifestFile) { var manifest = JsonSerializer.Deserialize(File.ReadAllText(manifestFile)) diff --git a/src/ServiceControl.Persistence/ServiceControl.Persistence.csproj b/src/ServiceControl.Persistence/ServiceControl.Persistence.csproj index f4f4299197..131f2cfa0e 100644 --- a/src/ServiceControl.Persistence/ServiceControl.Persistence.csproj +++ b/src/ServiceControl.Persistence/ServiceControl.Persistence.csproj @@ -16,4 +16,8 @@ + + + + \ No newline at end of file diff --git a/src/ServiceControl.UnitTests/Infrastructure/PersistenceManifestLoadingTests.cs b/src/ServiceControl.UnitTests/Infrastructure/PersistenceManifestLoadingTests.cs new file mode 100644 index 0000000000..d7222810b7 --- /dev/null +++ b/src/ServiceControl.UnitTests/Infrastructure/PersistenceManifestLoadingTests.cs @@ -0,0 +1,87 @@ +namespace ServiceControl.UnitTests.Infrastructure; + +using System.Collections.Generic; +using System.IO; +using System.Linq; +using NUnit.Framework; +using ServiceControl.Persistence; + +[TestFixture] +public class PersistenceManifestLoadingTests +{ + const string RavenDB = """ + { + "Name": "RavenDB", + "DisplayName": "RavenDB", + "Description": "RavenDB ServiceControl persister", + "AssemblyName": "ServiceControl.Persistence.RavenDB", + "TypeName": "ServiceControl.Persistence.RavenDB.RavenPersistenceConfiguration, ServiceControl.Persistence.RavenDB" + } + """; + + // Ships so that ServiceControl Management can describe pre-v5 instances, and has no assembly left to name + const string RavenDB35 = """ + { + "Name": "RavenDB35", + "IsSupported": false, + "DisplayName": "RavenDB 3.5 (Legacy)", + "Description": "RavenDB 3.5 (Legacy) ServiceControl persister" + } + """; + + const string SqlServer = """ + { + "Name": "SQLServer", + "DisplayName": "SQL Server", + "Description": "SQL Server ServiceControl persister", + "AssemblyName": "ServiceControl.Persistence.EFCore.SqlServer", + "TypeName": "ServiceControl.Persistence.EFCore.SqlServer.SqlServerPersistenceConfiguration, ServiceControl.Persistence.EFCore.SqlServer" + } + """; + + [Test] + public void Legacy_manifest_without_an_assembly_does_not_hide_the_persisters_after_it() + { + var manifests = LoadFrom(new() + { + ["RavenDB"] = RavenDB, + ["RavenDB35"] = RavenDB35, + ["SQLServer"] = SqlServer + }); + + Assert.That(manifests.Select(m => m.Name), Is.EquivalentTo(["RavenDB", "RavenDB35", "SQLServer"])); + } + + [Test] + public void Unreadable_manifest_does_not_hide_the_persisters_after_it() + { + var manifests = LoadFrom(new() + { + ["Corrupt"] = "{ this is not json", + ["SQLServer"] = SqlServer + }); + + Assert.That(manifests.Select(m => m.Name), Is.EqualTo(["SQLServer"])); + } + + static List LoadFrom(Dictionary persisters) + { + var installDirectory = Path.Combine(TestContext.CurrentContext.TestDirectory, TestContext.CurrentContext.Test.ID); + + foreach (var (persister, manifest) in persisters) + { + var persisterDirectory = Path.Combine(installDirectory, "Persisters", persister); + Directory.CreateDirectory(persisterDirectory); + File.WriteAllText(Path.Combine(persisterDirectory, "persistence.manifest"), manifest); + } + + try + { + return PersistenceManifestLibrary.LoadManifests(Directory.EnumerateFiles(installDirectory, "persistence.manifest", SearchOption.AllDirectories)); + } + finally + { + Directory.Delete(installDirectory, true); + } + } +}