From 709ac7c875fd698e41320986c96a1c0ecd8b269b Mon Sep 17 00:00:00 2001 From: Phil Bastian Date: Thu, 22 Oct 2026 12:38:59 +0800 Subject: [PATCH 1/3] fix round-tripping endpoint amendments --- .../LicensedEndpointDetails.cs | 1 + .../LicenseMetadata.cs | 1 + .../Throughput/LicensingDataStore.cs | 2 ++ 3 files changed, 4 insertions(+) diff --git a/src/Particular.LicensingComponent.Contracts/LicensedEndpointDetails.cs b/src/Particular.LicensingComponent.Contracts/LicensedEndpointDetails.cs index d61bd4abcf..57fa350fbf 100644 --- a/src/Particular.LicensingComponent.Contracts/LicensedEndpointDetails.cs +++ b/src/Particular.LicensingComponent.Contracts/LicensedEndpointDetails.cs @@ -41,6 +41,7 @@ public record LicensedEndpoint public string ClientId { get; set; } = Guid.NewGuid().ToString(); public required string Name { get; set; } public required int Classification { get; set; } + public bool Amended { get; set; } = false; public string? EndpointSize { get; set; } public QueueIdentity[] Queues { get; set; } = []; diff --git a/src/Particular.LicensingComponent.Persistence/LicenseMetadata.cs b/src/Particular.LicensingComponent.Persistence/LicenseMetadata.cs index 60079668bb..e167db2744 100644 --- a/src/Particular.LicensingComponent.Persistence/LicenseMetadata.cs +++ b/src/Particular.LicensingComponent.Persistence/LicenseMetadata.cs @@ -41,6 +41,7 @@ .. baseDetails.Endpoints.Where(endpoint => !amendments.RemovedEndpointIds.Contai { Name = amendment?.Name ?? endpoint.Name, Classification = amendment?.Classification ?? endpoint.Classification, + Amended = amendment is not null, Queues = [.. endpoint.Queues.Except( [ .. amendments.UnassignedQueues, diff --git a/src/ServiceControl.Persistence.RavenDB/Throughput/LicensingDataStore.cs b/src/ServiceControl.Persistence.RavenDB/Throughput/LicensingDataStore.cs index 61c50a1cec..9605b077b3 100644 --- a/src/ServiceControl.Persistence.RavenDB/Throughput/LicensingDataStore.cs +++ b/src/ServiceControl.Persistence.RavenDB/Throughput/LicensingDataStore.cs @@ -404,6 +404,8 @@ public async Task SaveLicensedEndpointDetailsAmendment(LicensedEndpointDetailsAm { // these do not round-trip result.UnmappedQueues = [.. existing.UnmappedQueues, .. result.UnmappedQueues]; + var amendedEndpointIds = result.Endpoints.Select(re => re.ClientId).ToArray(); + result.Endpoints = [.. existing.Endpoints.Where(endpoint => !amendedEndpointIds.Contains(endpoint.ClientId)), .. result.Endpoints]; session.Advanced.Evict(existing); } From a3544fab407f2b966a3e72a8cb229e635185ba44 Mon Sep 17 00:00:00 2001 From: Phil Bastian Date: Thu, 22 Oct 2026 14:35:35 +0800 Subject: [PATCH 2/3] apply fix for round-tripping endpoint amendments to SQL and in-memory data stores --- .../InMemoryLicensingDataStore.cs | 2 ++ .../Implementation/LicensingDataStore.cs | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/Particular.LicensingComponent.Persistence.InMemory/InMemoryLicensingDataStore.cs b/src/Particular.LicensingComponent.Persistence.InMemory/InMemoryLicensingDataStore.cs index 11669f9cd4..6e98687dcb 100644 --- a/src/Particular.LicensingComponent.Persistence.InMemory/InMemoryLicensingDataStore.cs +++ b/src/Particular.LicensingComponent.Persistence.InMemory/InMemoryLicensingDataStore.cs @@ -201,6 +201,8 @@ public Task SaveLicensedEndpointDetails(LicensedEndpointDetails result, Cancella public Task SaveLicensedEndpointDetailsAmendment(LicensedEndpointDetailsAmendment? result, CancellationToken cancellationToken = default) { result?.UnmappedQueues = [.. endpointDetailsAmendment?.UnmappedQueues ?? [], .. result.UnmappedQueues]; + var amendedEndpointIds = result?.Endpoints.Select(re => re.ClientId).ToArray() ?? []; + result?.Endpoints = [.. endpointDetailsAmendment?.Endpoints.Where(endpoint => !amendedEndpointIds.Contains(endpoint.ClientId)) ?? [], .. result.Endpoints]; endpointDetailsAmendment = result; return Task.CompletedTask; } diff --git a/src/ServiceControl.Persistence.EFCore/Implementation/LicensingDataStore.cs b/src/ServiceControl.Persistence.EFCore/Implementation/LicensingDataStore.cs index 5a359a7a9a..e65def8842 100644 --- a/src/ServiceControl.Persistence.EFCore/Implementation/LicensingDataStore.cs +++ b/src/ServiceControl.Persistence.EFCore/Implementation/LicensingDataStore.cs @@ -350,6 +350,8 @@ public Task SaveLicensedEndpointDetailsAmendment(LicensedEndpointDetailsAmendmen } // these do not round-trip result.UnmappedQueues = [.. existing.UnmappedQueues, .. result.UnmappedQueues]; + var amendedEndpointIds = result.Endpoints.Select(re => re.ClientId).ToArray(); + result.Endpoints = [.. existing.Endpoints.Where(endpoint => !amendedEndpointIds.Contains(endpoint.ClientId)), .. result.Endpoints]; } await context.StoreSetting(SettingKeys.LicensedEndpointDetailsAmendment, result, token); }, cancellationToken); From 73811395db68672a5e71332be2654546d3ca1995 Mon Sep 17 00:00:00 2001 From: Phil Bastian Date: Fri, 23 Oct 2026 07:47:22 +0800 Subject: [PATCH 3/3] extract common functionality and remove unneeded merge of amended endpoints (since amended endpoints now do round-trip) --- .../InMemoryLicensingDataStore.cs | 4 +--- .../LicenseMetadata.cs | 14 ++++++++++++++ .../Implementation/LicensingDataStore.cs | 5 +---- .../Throughput/LicensingDataStore.cs | 5 +---- 4 files changed, 17 insertions(+), 11 deletions(-) diff --git a/src/Particular.LicensingComponent.Persistence.InMemory/InMemoryLicensingDataStore.cs b/src/Particular.LicensingComponent.Persistence.InMemory/InMemoryLicensingDataStore.cs index 6e98687dcb..9567b97bce 100644 --- a/src/Particular.LicensingComponent.Persistence.InMemory/InMemoryLicensingDataStore.cs +++ b/src/Particular.LicensingComponent.Persistence.InMemory/InMemoryLicensingDataStore.cs @@ -200,9 +200,7 @@ public Task SaveLicensedEndpointDetails(LicensedEndpointDetails result, Cancella public Task SaveLicensedEndpointDetailsAmendment(LicensedEndpointDetailsAmendment? result, CancellationToken cancellationToken = default) { - result?.UnmappedQueues = [.. endpointDetailsAmendment?.UnmappedQueues ?? [], .. result.UnmappedQueues]; - var amendedEndpointIds = result?.Endpoints.Select(re => re.ClientId).ToArray() ?? []; - result?.Endpoints = [.. endpointDetailsAmendment?.Endpoints.Where(endpoint => !amendedEndpointIds.Contains(endpoint.ClientId)) ?? [], .. result.Endpoints]; + result.MergeWithExisting(endpointDetailsAmendment); endpointDetailsAmendment = result; return Task.CompletedTask; } diff --git a/src/Particular.LicensingComponent.Persistence/LicenseMetadata.cs b/src/Particular.LicensingComponent.Persistence/LicenseMetadata.cs index e167db2744..3bde1d3c72 100644 --- a/src/Particular.LicensingComponent.Persistence/LicenseMetadata.cs +++ b/src/Particular.LicensingComponent.Persistence/LicenseMetadata.cs @@ -65,5 +65,19 @@ .. amendments.Endpoints.Where(endpoint => !baseDetails.Endpoints.Any(baseEndpoin return baseDetails; } } + + extension(LicensedEndpointDetailsAmendment? amendment) + { + public void MergeWithExisting(LicensedEndpointDetailsAmendment? existing) + { + if (amendment is null || existing is null) + { + return; + } + + // these do not round-trip the full collections, so reapply what we already have + amendment.UnmappedQueues = [.. existing.UnmappedQueues, .. amendment.UnmappedQueues]; + } + } } } diff --git a/src/ServiceControl.Persistence.EFCore/Implementation/LicensingDataStore.cs b/src/ServiceControl.Persistence.EFCore/Implementation/LicensingDataStore.cs index e65def8842..806869607d 100644 --- a/src/ServiceControl.Persistence.EFCore/Implementation/LicensingDataStore.cs +++ b/src/ServiceControl.Persistence.EFCore/Implementation/LicensingDataStore.cs @@ -348,10 +348,7 @@ public Task SaveLicensedEndpointDetailsAmendment(LicensedEndpointDetailsAmendmen await context.SaveChangesAsync(cancellationToken); return; } - // these do not round-trip - result.UnmappedQueues = [.. existing.UnmappedQueues, .. result.UnmappedQueues]; - var amendedEndpointIds = result.Endpoints.Select(re => re.ClientId).ToArray(); - result.Endpoints = [.. existing.Endpoints.Where(endpoint => !amendedEndpointIds.Contains(endpoint.ClientId)), .. result.Endpoints]; + result.MergeWithExisting(existing); } await context.StoreSetting(SettingKeys.LicensedEndpointDetailsAmendment, result, token); }, cancellationToken); diff --git a/src/ServiceControl.Persistence.RavenDB/Throughput/LicensingDataStore.cs b/src/ServiceControl.Persistence.RavenDB/Throughput/LicensingDataStore.cs index 9605b077b3..1641d4856b 100644 --- a/src/ServiceControl.Persistence.RavenDB/Throughput/LicensingDataStore.cs +++ b/src/ServiceControl.Persistence.RavenDB/Throughput/LicensingDataStore.cs @@ -402,10 +402,7 @@ public async Task SaveLicensedEndpointDetailsAmendment(LicensedEndpointDetailsAm var existing = await session.LoadAsync(LicencedEndpointDetailsAmendmentDocumentId, cancellationToken); if (existing is not null) { - // these do not round-trip - result.UnmappedQueues = [.. existing.UnmappedQueues, .. result.UnmappedQueues]; - var amendedEndpointIds = result.Endpoints.Select(re => re.ClientId).ToArray(); - result.Endpoints = [.. existing.Endpoints.Where(endpoint => !amendedEndpointIds.Contains(endpoint.ClientId)), .. result.Endpoints]; + result.MergeWithExisting(existing); session.Advanced.Evict(existing); }