diff --git a/src/Microsoft.Health.Fhir.Shared.Core.UnitTests/Features/Validation/ServerProvideProfileValidationTests.cs b/src/Microsoft.Health.Fhir.Shared.Core.UnitTests/Features/Validation/ServerProvideProfileValidationTests.cs index 84d5d45b6d..54ba207693 100644 --- a/src/Microsoft.Health.Fhir.Shared.Core.UnitTests/Features/Validation/ServerProvideProfileValidationTests.cs +++ b/src/Microsoft.Health.Fhir.Shared.Core.UnitTests/Features/Validation/ServerProvideProfileValidationTests.cs @@ -111,6 +111,38 @@ public async Task GivenStructureDefinitionsExist_WhenGettingSupportedProfiles_Th Assert.Contains("http://example.org/fhir/StructureDefinition/custom-patient", profiles); } + [Fact] + public async Task GivenVersionedStructureDefinition_WhenGettingSupportedProfiles_ThenVersionedCanonicalIsReturned() + { + // Arrange + var patientProfile = CreateStructureDefinition("http://example.org/fhir/StructureDefinition/custom-patient", "Patient", "3.0.0"); + SetupSearchServiceWithResults("StructureDefinition", patientProfile); + + // Act + var profiles = await _serverProvideProfileValidation.GetSupportedProfilesAsync("Patient", CancellationToken.None); + + // Assert + Assert.Single(profiles); + Assert.Contains("http://example.org/fhir/StructureDefinition/custom-patient|3.0.0", profiles); + } + + [Fact] + public async Task GivenVersionedStructureDefinition_WhenResolvingByCanonicalUriWithVersion_ThenMatchingProfileIsReturned() + { + // Arrange + var patientProfile = CreateStructureDefinition("http://example.org/fhir/StructureDefinition/custom-patient", "Patient", "3.0.0"); + SetupSearchServiceWithResults("StructureDefinition", patientProfile); + + // Act + var profile = await _serverProvideProfileValidation.ResolveByCanonicalUriAsync("http://example.org/fhir/StructureDefinition/custom-patient|3.0.0"); + + // Assert + Assert.NotNull(profile); + var structureDefinition = Assert.IsType(profile); + Assert.Equal("http://example.org/fhir/StructureDefinition/custom-patient", structureDefinition.Url); + Assert.Equal("3.0.0", structureDefinition.Version); + } + [Fact] public async Task GivenANewStructureDefinition_WhenBackgroundLoopRuns_ThenSyncIsRequested() { @@ -353,12 +385,13 @@ public void Dispose() _serverProvideProfileValidation?.Dispose(); } - private static StructureDefinition CreateStructureDefinition(string url, string type) + private static StructureDefinition CreateStructureDefinition(string url, string type, string version = null) { return new StructureDefinition { Id = Guid.NewGuid().ToString("N").Substring(0, 16), // Generate valid FHIR ID Url = url, + Version = version, Name = $"{type}Profile", Status = PublicationStatus.Active, Kind = StructureDefinition.StructureDefinitionKind.Resource, diff --git a/src/Microsoft.Health.Fhir.Shared.Core/Features/Validation/ServerProvideProfileValidation.cs b/src/Microsoft.Health.Fhir.Shared.Core/Features/Validation/ServerProvideProfileValidation.cs index 8f7ebb7582..c0a9652c0d 100644 --- a/src/Microsoft.Health.Fhir.Shared.Core/Features/Validation/ServerProvideProfileValidation.cs +++ b/src/Microsoft.Health.Fhir.Shared.Core/Features/Validation/ServerProvideProfileValidation.cs @@ -138,7 +138,18 @@ public void Dispose() public async Task ResolveByCanonicalUriAsync(string uri) { - var summary = (await ListSummariesAsync(CancellationToken.None)).ResolveByCanonicalUri(uri); + var summaries = (await ListSummariesAsync(CancellationToken.None)).ToList(); + var summary = summaries.ResolveByCanonicalUri(uri); + + if (summary == null && + TrySplitVersionedCanonicalUri(uri, out string canonicalUri, out string version)) + { + summary = summaries.FirstOrDefault(x => + string.Equals(x.ResourceUri, canonicalUri, StringComparison.OrdinalIgnoreCase) && + x.TryGetValue(_structureDefinitionVersionKey, out object summaryVersion) && + string.Equals(summaryVersion?.ToString(), version, StringComparison.OrdinalIgnoreCase)); + } + return LoadBySummary(summary); } @@ -175,6 +186,30 @@ private static string GetCanonicalUrl(ArtifactSummary artifact) return url; } + private static bool TrySplitVersionedCanonicalUri(string uri, out string canonicalUri, out string version) + { + canonicalUri = uri; + version = null; + + if (string.IsNullOrWhiteSpace(uri)) + { + return false; + } + + int fragmentIndex = uri.IndexOf('#'); + string uriWithoutFragment = fragmentIndex >= 0 ? uri.Substring(0, fragmentIndex) : uri; + int versionSeparatorIndex = uriWithoutFragment.LastIndexOf('|'); + + if (versionSeparatorIndex <= 0 || versionSeparatorIndex == uriWithoutFragment.Length - 1) + { + return false; + } + + canonicalUri = uriWithoutFragment.Substring(0, versionSeparatorIndex); + version = uriWithoutFragment.Substring(versionSeparatorIndex + 1); + return true; + } + private static string GetHashForSupportedProfiles(IReadOnlyCollection summaries) { if (summaries == null)