Skip to content

Commit cc4de9a

Browse files
authored
Changes to include Structure Definition version in canonical URL (#5280)
* CHanges to include Structure Definition version in canonical URL, as well as unit test. * Rolled back so only one (latest) structure definition is returned in capability statement. * Modified Unit test to check for single version returned. * updated E2E tests to include version for structured definitions
1 parent 407da35 commit cc4de9a

File tree

4 files changed

+94
-12
lines changed

4 files changed

+94
-12
lines changed

docs/rest/VersionedUpdateExample.http

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,4 +228,28 @@ If-Match: W/"2"
228228
}
229229
]
230230
}
231+
}
232+
233+
###
234+
# Post a us-core-patient profile
235+
# This will be returned in capability statement.
236+
# @name patient
237+
POST https://{{hostname}}/StructureDefinition
238+
Content-Type: application/json
239+
Authorization: Bearer {{bearer.response.body.access_token}}
240+
241+
{
242+
"resourceType": "StructureDefinition",
243+
"id": "us-core-patient",
244+
"url": "http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient",
245+
"version": "6.1.0",
246+
"name": "USCorePatientProfile",
247+
"title": "US Core Patient Profile",
248+
"status": "active",
249+
"experimental": false,
250+
"date": "2025-12-15",
251+
"publisher": "HL7 US Realm Steering Committee",
252+
"type": "Patient",
253+
"kind": "resource",
254+
"abstract": false,
231255
}

src/Microsoft.Health.Fhir.R4.Core.UnitTests/Features/Validation/ServerProvideProfileValidationTests.cs

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,23 @@ public async Task GivenCaseInsensitiveResourceType_WhenGettingSupportedProfiles_
250250
Assert.Contains("http://example.org/fhir/StructureDefinition/custom-patient", profiles);
251251
}
252252

253+
[Fact]
254+
public async Task GivenMultipleVersionsOfSameProfile_WhenGettingSupportedProfiles_ThenAllVersionsAreReturned()
255+
{
256+
// Arrange
257+
var patientProfileV1 = CreateStructureDefinition("http://example.org/fhir/StructureDefinition/custom-patient", "Patient", "1.0.0");
258+
var patientProfileV2 = CreateStructureDefinition("http://example.org/fhir/StructureDefinition/custom-patient", "Patient", "2.0.0");
259+
SetupSearchServiceWithResults("StructureDefinition", patientProfileV1, patientProfileV2);
260+
261+
// Act
262+
var profiles = await _serverProvideProfileValidation.GetSupportedProfilesAsync("Patient", CancellationToken.None);
263+
264+
// Assert
265+
Assert.NotNull(profiles);
266+
Assert.Single(profiles);
267+
Assert.Contains("http://example.org/fhir/StructureDefinition/custom-patient|2.0.0", profiles);
268+
}
269+
253270
[Fact]
254271
public async Task GivenDisableCacheRefresh_WhenGettingSupportedProfiles_ThenCacheIsNotRefreshed()
255272
{
@@ -276,9 +293,9 @@ public void Dispose()
276293
_serverProvideProfileValidation?.Dispose();
277294
}
278295

279-
private static StructureDefinition CreateStructureDefinition(string url, string type)
296+
private static StructureDefinition CreateStructureDefinition(string url, string type, string version = null)
280297
{
281-
return new StructureDefinition
298+
var structureDefinition = new StructureDefinition
282299
{
283300
Id = Guid.NewGuid().ToString("N").Substring(0, 16), // Generate valid FHIR ID
284301
Url = url,
@@ -290,6 +307,13 @@ private static StructureDefinition CreateStructureDefinition(string url, string
290307
BaseDefinition = $"http://hl7.org/fhir/StructureDefinition/{type}",
291308
Derivation = StructureDefinition.TypeDerivationRule.Constraint,
292309
};
310+
311+
if (!string.IsNullOrEmpty(version))
312+
{
313+
structureDefinition.Version = version;
314+
}
315+
316+
return structureDefinition;
293317
}
294318

295319
private void SetupSearchServiceWithNoResults()

src/Microsoft.Health.Fhir.Shared.Core/Features/Validation/ServerProvideProfileValidation.cs

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
using Hl7.Fhir.ElementModel;
1616
using Hl7.Fhir.Model;
1717
using Hl7.Fhir.Serialization;
18+
using Hl7.Fhir.Specification;
1819
using Hl7.Fhir.Specification.Source;
1920
using Hl7.Fhir.Specification.Summary;
2021
using MediatR;
@@ -37,6 +38,7 @@ namespace Microsoft.Health.Fhir.Core.Features.Validation
3738
public sealed class ServerProvideProfileValidation : IProvideProfilesForValidation, IDisposable
3839
{
3940
private static HashSet<string> _supportedTypes = new HashSet<string>() { "ValueSet", "StructureDefinition", "CodeSystem" };
41+
private static string _structureDefinitionVersionKey = "Conformance.version";
4042

4143
private readonly SemaphoreSlim _cacheSemaphore = new SemaphoreSlim(1, 1);
4244
private readonly Func<IScoped<ISearchService>> _searchServiceFactory;
@@ -227,7 +229,18 @@ public async Task<IEnumerable<string>> GetSupportedProfilesAsync(string resource
227229

228230
return string.Equals((string)type, resourceType, StringComparison.OrdinalIgnoreCase);
229231
})
230-
.Select(x => x.ResourceUri).ToList();
232+
.Select(x => GetCanonicalUrl(x)).ToList();
233+
}
234+
235+
private static string GetCanonicalUrl(ArtifactSummary artifact)
236+
{
237+
var url = artifact.ResourceUri;
238+
if (artifact.TryGetValue(_structureDefinitionVersionKey, out object version) && version != null && !string.IsNullOrEmpty(version.ToString()))
239+
{
240+
return $"{url}|{version}";
241+
}
242+
243+
return url;
231244
}
232245

233246
private static string GetHashForSupportedProfiles(IReadOnlyCollection<ArtifactSummary> summaries)
@@ -240,7 +253,7 @@ private static string GetHashForSupportedProfiles(IReadOnlyCollection<ArtifactSu
240253
var sb = new StringBuilder();
241254
summaries.Where(x => x.ResourceTypeName == KnownResourceTypes.StructureDefinition)
242255
.Where(x => x.TryGetValue(StructureDefinitionSummaryProperties.TypeKey, out object type))
243-
.Select(x => x.ResourceUri).ToList().ForEach(url => sb.Append(url));
256+
.Select(x => GetCanonicalUrl(x)).ToList().ForEach(url => sb.Append(url));
244257

245258
return sb.ToString().ComputeHash();
246259
}

test/Microsoft.Health.Fhir.Shared.Tests.E2E/Rest/ValidateTests.cs

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -221,22 +221,43 @@ public async Task GivenInvalidProfile_WhenValidateCalled_ThenBadRequestReturned(
221221
[Fact]
222222
public async Task GivenPostedProfiles_WhenCallingForMetadata_ThenMetadataHasSupportedProfiles()
223223
{
224+
// Give the server time to refresh its profile cache
225+
await Task.Delay(TimeSpan.FromSeconds(2));
226+
224227
using FhirResponse<CapabilityStatement> response = await _client.ReadAsync<CapabilityStatement>("metadata");
225228
#if !Stu3
226229
var supportedProfiles = response.Resource.Rest.Where(r => r.Mode.ToString().Equals("server", StringComparison.OrdinalIgnoreCase)).
227230
SelectMany(x => x.Resource.Where(x => x.SupportedProfile.Any()).Select(x => x.SupportedProfile)).
228231
SelectMany(x => x).OrderBy(x => x).ToList();
232+
233+
var expectedProfiles = new[]
234+
{
235+
"http://hl7.org/fhir/us/core/StructureDefinition/us-core-careplan|3.0.0",
236+
"http://hl7.org/fhir/us/core/StructureDefinition/us-core-organization|3.0.0",
237+
"http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient|3.0.0",
238+
};
229239
#else
230240
var supportedProfiles = response.Resource.Profile.Select(x => x.Url.ToString()).OrderBy(x => x).ToList();
241+
242+
var expectedProfiles = new[]
243+
{
244+
"http://hl7.org/fhir/us/core/StructureDefinition/us-core-careplan|2.0.0",
245+
"http://hl7.org/fhir/us/core/StructureDefinition/us-core-organization|2.0.0",
246+
"http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient|2.0.0",
247+
};
231248
#endif
232-
Assert.All(
233-
new[]
234-
{
235-
"http://hl7.org/fhir/us/core/StructureDefinition/us-core-careplan",
236-
"http://hl7.org/fhir/us/core/StructureDefinition/us-core-organization",
237-
"http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient",
238-
},
239-
x => Assert.Contains(supportedProfiles, y => string.Equals(x, y, StringComparison.OrdinalIgnoreCase)));
249+
250+
// Add this to see what profiles are actually returned
251+
var actualProfilesString = string.Join(", ", supportedProfiles);
252+
System.Diagnostics.Debug.WriteLine($"Actual profiles returned: {actualProfilesString}");
253+
254+
// Check each expected profile individually to see which one is missing
255+
foreach (var expectedProfile in expectedProfiles)
256+
{
257+
var found = supportedProfiles.Any(y => string.Equals(expectedProfile, y, StringComparison.OrdinalIgnoreCase));
258+
System.Diagnostics.Debug.WriteLine($"Profile '{expectedProfile}' found: {found}");
259+
Assert.Contains(supportedProfiles, y => string.Equals(expectedProfile, y, StringComparison.OrdinalIgnoreCase));
260+
}
240261
}
241262

242263
private void CheckOperationOutcomeIssue(

0 commit comments

Comments
 (0)