Skip to content

Conversation

@brendankowitz
Copy link
Member

Description

This pull request introduces foundational changes to enable migration from the Firely SDK to the Ignixa SDK for FHIR resource handling. The update includes infrastructure changes to drop .NET 8.0 support, upgrade the Firely SDK, add Ignixa package dependencies, and document the migration approach, tradeoffs, and prototype results. The Ignixa serialization formatters are now configured as the primary JSON handlers, allowing for a phased, backward-compatible migration.

Migration infrastructure and SDK upgrades:

  • Dropped .NET 8.0 support, making net9.0 the only target framework in Directory.Build.props to align with Ignixa's requirements.
  • Upgraded Firely SDK from version 5.11.4 to 5.13.1 in Directory.Packages.props to resolve compatibility issues with Ignixa shims.
  • Added Ignixa SDK package references (Ignixa.Abstractions, Ignixa.Serialization, Ignixa.Extensions.FirelySdk5, etc.) to the solution for FHIR resource handling, serialization, and validation.

Build and dependency management:

  • Removed conditional package version logic for ASP.NET and SDK packages, now using only .NET 9.0-specific versions (e.g., AspNetPackageVersion set to 9.0.3).
  • Ensured solution file project ordering is correct and consistent, with minor adjustments to project listing in R4.slnf. [1] [2]

Documentation and migration planning:

  • Added a comprehensive investigation document (complete-ignixa-replacement.md) outlining the approach, tradeoffs, migration phases, prototype findings, and next steps for replacing Firely SDK with Ignixa. This document details the rationale, architectural changes, and compatibility considerations for the migration.

Related issues

Addresses [issue #].

Testing

Describe how this change was tested.

FHIR Team Checklist

  • Update the title of the PR to be succinct and less than 65 characters
  • Add a milestone to the PR for the sprint that it is merged (i.e. add S47)
  • Tag the PR with the type of update: Bug, Build, Dependencies, Enhancement, New-Feature or Documentation
  • Tag the PR with Open source, Azure API for FHIR (CosmosDB or common code) or Azure Healthcare APIs (SQL or common code) to specify where this change is intended to be released.
  • Tag the PR with Schema Version backward compatible or Schema Version backward incompatible or Schema Version unchanged if this adds or updates Sql script which is/is not backward compatible with the code.
  • When changing or adding behavior, if your code modifies the system design or changes design assumptions, please create and include an ADR.
  • CI is green before merge Build Status
  • Review squash-merge requirements

Semver Change (docs)

Patch|Skip|Feature|Breaking (reason)

brendankowitz and others added 4 commits December 30, 2025 16:33
- Added Microsoft.Health.Fhir.Ignixa project to Dockerfile for proper restore
- Removed Linux_dotnet8 build jobs from pr-pipeline.yml and ci-pipeline.yml
- Updated .vsts-PRInternalChecks-azureBuild-pipeline.yml to use net9.0
- Updated SDK version to 9.0.308 in global.json

These changes are needed to complete the migration to .NET 9.0-only and
ensure the Ignixa SDK integration works correctly in CI/CD pipelines.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
@brendankowitz brendankowitz force-pushed the personal/bkowitz/ignixa-sdk branch from 5c61cca to 67011b0 Compare January 6, 2026 18:22
// Try conversion for common types
if (value != null && typeof(T) == typeof(string))
{
return (T)(object)value.ToString();

Check warning

Code scanning / CodeQL

Useless upcast

There is no need to upcast from [String](1) to [Object](2) - the conversion can be done implicitly.

Copilot Autofix

AI 4 days ago

In general, to fix a useless upcast, you remove the unnecessary explicit cast and rely on the compiler’s implicit conversion, as long as it doesn’t change overload resolution or behavior. Here, we’re converting value.ToString() to T under the condition typeof(T) == typeof(string). The current code performs this as return (T)(object)value.ToString();, explicitly upcasting the string to object first. Since string is a reference type, the cast to object is implicit and not needed.

The best localized fix that keeps existing functionality is to remove the (object) cast and directly cast the string to T via (T)(object) only if you wanted double-cast, but since T is known to be string, we can safely cast the expression directly to T. The simplest compatible fix is:

return (T)(object)value.ToString();

return (T)(object)value.ToString();

would still have the (object) upcast, so instead we should remove just the (object) and cast directly to T:

return (T)(object)value.ToString();

becomes

return (T)(object)value.ToString();

However, that still leaves an (object) cast. The truly redundant part is the upcast to object; we only need to convert the string result to T. Given typeof(T) == typeof(string), the compiler will accept return (T)(object)value.ToString(); but the (object) is unnecessary. The cleanest form is:

return (T)(object)value.ToString();

Actually, we can remove the upcast and directly cast the string to T by:

return (T)(object)value.ToString();

But we must respect the CodeQL report specifically about (object). The minimal and correct change is:

return (T)(object)value.ToString();

return (T)value.ToString();

All logic and type checks remain identical, and no new imports or helpers are needed. Only line 64 in FirelyCompiledFhirPath.cs changes.

Suggested changeset 1
src/Microsoft.Health.Fhir.Core/Features/Search/FhirPath/FirelyCompiledFhirPath.cs

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/src/Microsoft.Health.Fhir.Core/Features/Search/FhirPath/FirelyCompiledFhirPath.cs b/src/Microsoft.Health.Fhir.Core/Features/Search/FhirPath/FirelyCompiledFhirPath.cs
--- a/src/Microsoft.Health.Fhir.Core/Features/Search/FhirPath/FirelyCompiledFhirPath.cs
+++ b/src/Microsoft.Health.Fhir.Core/Features/Search/FhirPath/FirelyCompiledFhirPath.cs
@@ -61,7 +61,7 @@
             // Try conversion for common types
             if (value != null && typeof(T) == typeof(string))
             {
-                return (T)(object)value.ToString();
+                return (T)value.ToString();
             }
 
             return default;
EOF
@@ -61,7 +61,7 @@
// Try conversion for common types
if (value != null && typeof(T) == typeof(string))
{
return (T)(object)value.ToString();
return (T)value.ToString();
}

return default;
Copilot is powered by AI and may make mistakes. Always verify output.
// Try conversion for common types
if (value != null && typeof(T) == typeof(string))
{
return (T)(object)value.ToString()!;

Check warning

Code scanning / CodeQL

Useless upcast

There is no need to upcast from [String](1) to [Object](2) - the conversion can be done implicitly.

Copilot Autofix

AI 4 days ago

To fix the problem, remove the unnecessary upcast to object while preserving the outer cast to T, since the method must return T. The general rule is: when a value of a reference type is being explicitly cast to object only to be immediately cast again to some other type, the intermediate (object) is redundant and should be removed.

Specifically, in IgnixaCompiledFhirPath.Scalar<T>, at line 95:

return (T)(object)value.ToString()!;

we should remove the (object) part and keep:

return (T)value.ToString()!;

This keeps the existing functionality (attempting to treat the string representation as T when typeof(T) == typeof(string)) but eliminates the useless upcast. No new imports, methods, or other definitions are needed. Only that single line in src/Microsoft.Health.Fhir.Ignixa/FhirPath/IgnixaCompiledFhirPath.cs should change.

Suggested changeset 1
src/Microsoft.Health.Fhir.Ignixa/FhirPath/IgnixaCompiledFhirPath.cs

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/src/Microsoft.Health.Fhir.Ignixa/FhirPath/IgnixaCompiledFhirPath.cs b/src/Microsoft.Health.Fhir.Ignixa/FhirPath/IgnixaCompiledFhirPath.cs
--- a/src/Microsoft.Health.Fhir.Ignixa/FhirPath/IgnixaCompiledFhirPath.cs
+++ b/src/Microsoft.Health.Fhir.Ignixa/FhirPath/IgnixaCompiledFhirPath.cs
@@ -92,7 +92,7 @@
         // Try conversion for common types
         if (value != null && typeof(T) == typeof(string))
         {
-            return (T)(object)value.ToString()!;
+            return (T)value.ToString()!;
         }
 
         return default;
EOF
@@ -92,7 +92,7 @@
// Try conversion for common types
if (value != null && typeof(T) == typeof(string))
{
return (T)(object)value.ToString()!;
return (T)value.ToString()!;
}

return default;
Copilot is powered by AI and may make mistakes. Always verify output.
Unable to commit as this autofix suggestion is now outdated
ValidateResourceId(resourceNode?.Id);
CheckConditionalReferenceInResource(resourceNode, importMode);

var lastUpdatedIsNull = importMode == ImportMode.InitialLoad || resourceNode.Meta.LastUpdated == null;

Check warning

Code scanning / CodeQL

Dereferenced variable may be null

Variable [resourceNode](1) may be null at this access as suggested by [this](2) null check.

Copilot Autofix

AI 4 days ago

In general, the fix is to ensure that resourceNode is known to be non-null before any dereference, either by validating and throwing a clear exception when it is null, or by returning early. This retains existing logic while preventing a potential NullReferenceException.

The best minimal fix here is to add an explicit null check immediately after parsing rawResource. If _serializer.Parse(rawResource) returns null, we should throw a descriptive exception (for example, ArgumentException or InvalidOperationException) rather than continuing. Once we do a standard if (resourceNode == null) throw ...; check, the C# compiler and analysis tools know resourceNode is non-null afterwards, and all subsequent uses of resourceNode.Meta... are safe. This change should be placed right after line 41 in ImportResourceParser.Parse and before any access to resourceNode members. No new imports are needed, since System is already imported and provides InvalidOperationException / ArgumentException.

Concretely: in src/Microsoft.Health.Fhir.Shared.Core/Features/Operations/Import/ImportResourceParser.cs, inside Parse, add a guard:

var resourceNode = _serializer.Parse(rawResource);
if (resourceNode == null)
{
    throw new InvalidOperationException("Failed to parse raw resource into a FHIR resource node.");
}

Then leave the remainder of the method unchanged. This preserves existing behavior for valid inputs and replaces potential NullReferenceExceptions for invalid inputs with a clear, intentional exception.

Suggested changeset 1
src/Microsoft.Health.Fhir.Shared.Core/Features/Operations/Import/ImportResourceParser.cs

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/src/Microsoft.Health.Fhir.Shared.Core/Features/Operations/Import/ImportResourceParser.cs b/src/Microsoft.Health.Fhir.Shared.Core/Features/Operations/Import/ImportResourceParser.cs
--- a/src/Microsoft.Health.Fhir.Shared.Core/Features/Operations/Import/ImportResourceParser.cs
+++ b/src/Microsoft.Health.Fhir.Shared.Core/Features/Operations/Import/ImportResourceParser.cs
@@ -39,7 +39,11 @@
         public ImportResource Parse(long index, long offset, int length, string rawResource, ImportMode importMode)
         {
             var resourceNode = _serializer.Parse(rawResource);
-            ValidateResourceId(resourceNode?.Id);
+            if (resourceNode == null)
+            {
+                throw new InvalidOperationException("Failed to parse raw resource into a FHIR resource node.");
+            }
+            ValidateResourceId(resourceNode.Id);
             CheckConditionalReferenceInResource(resourceNode, importMode);
 
             var lastUpdatedIsNull = importMode == ImportMode.InitialLoad || resourceNode.Meta.LastUpdated == null;
EOF
@@ -39,7 +39,11 @@
public ImportResource Parse(long index, long offset, int length, string rawResource, ImportMode importMode)
{
var resourceNode = _serializer.Parse(rawResource);
ValidateResourceId(resourceNode?.Id);
if (resourceNode == null)
{
throw new InvalidOperationException("Failed to parse raw resource into a FHIR resource node.");
}
ValidateResourceId(resourceNode.Id);
CheckConditionalReferenceInResource(resourceNode, importMode);

var lastUpdatedIsNull = importMode == ImportMode.InitialLoad || resourceNode.Meta.LastUpdated == null;
Copilot is powered by AI and may make mistakes. Always verify output.

var lastUpdatedIsNull = importMode == ImportMode.InitialLoad || resourceNode.Meta.LastUpdated == null;
var lastUpdated = lastUpdatedIsNull ? Clock.UtcNow : resourceNode.Meta.LastUpdated.Value;
resourceNode.Meta.LastUpdated = new DateTimeOffset(lastUpdated.DateTime.TruncateToMillisecond(), lastUpdated.Offset);

Check warning

Code scanning / CodeQL

Dereferenced variable may be null

Variable [resourceNode](1) may be null at this access as suggested by [this](2) null check.

Copilot Autofix

AI 4 days ago

In general, we should ensure that resourceNode (and its nested Meta object) are not null before any dereference. Either we (a) validate the result of _serializer.Parse(rawResource) and throw a clear exception if it is null, or (b) adjust the serializer API contract. Since we must not change external behavior beyond avoiding a NullReferenceException, the best fix in this snippet is to add an explicit null check for resourceNode immediately after parsing, and to ensure Meta is initialized if it can be null.

Concretely, in ImportResourceParser.Parse in ImportResourceParser.cs, right after var resourceNode = _serializer.Parse(rawResource);, insert a guard:

  • If resourceNode is null, throw an informative FormatException or ArgumentException indicating that the raw resource is invalid.
  • Before accessing resourceNode.Meta, ensure it is non-null. If the FHIR node type permits Meta to be null for new resources, we can lazily initialize it (e.g., resourceNode.Meta ??= new Meta();) so behavior aligns with typical FHIR handling and avoids NREs.

This keeps existing semantics: previously, malformed resources would crash with a NullReferenceException at line 45–47; now they fail fast with a clearer exception, and well-formed resources continue to behave the same. No new external dependencies are required; we only use standard exception types from System.

Suggested changeset 1
src/Microsoft.Health.Fhir.Shared.Core/Features/Operations/Import/ImportResourceParser.cs

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/src/Microsoft.Health.Fhir.Shared.Core/Features/Operations/Import/ImportResourceParser.cs b/src/Microsoft.Health.Fhir.Shared.Core/Features/Operations/Import/ImportResourceParser.cs
--- a/src/Microsoft.Health.Fhir.Shared.Core/Features/Operations/Import/ImportResourceParser.cs
+++ b/src/Microsoft.Health.Fhir.Shared.Core/Features/Operations/Import/ImportResourceParser.cs
@@ -39,9 +39,20 @@
         public ImportResource Parse(long index, long offset, int length, string rawResource, ImportMode importMode)
         {
             var resourceNode = _serializer.Parse(rawResource);
-            ValidateResourceId(resourceNode?.Id);
+
+            if (resourceNode == null)
+            {
+                throw new FormatException("The provided raw resource could not be parsed into a valid FHIR resource.");
+            }
+
+            ValidateResourceId(resourceNode.Id);
             CheckConditionalReferenceInResource(resourceNode, importMode);
 
+            if (resourceNode.Meta == null)
+            {
+                resourceNode.Meta = new Meta();
+            }
+
             var lastUpdatedIsNull = importMode == ImportMode.InitialLoad || resourceNode.Meta.LastUpdated == null;
             var lastUpdated = lastUpdatedIsNull ? Clock.UtcNow : resourceNode.Meta.LastUpdated.Value;
             resourceNode.Meta.LastUpdated = new DateTimeOffset(lastUpdated.DateTime.TruncateToMillisecond(), lastUpdated.Offset);
EOF
@@ -39,9 +39,20 @@
public ImportResource Parse(long index, long offset, int length, string rawResource, ImportMode importMode)
{
var resourceNode = _serializer.Parse(rawResource);
ValidateResourceId(resourceNode?.Id);

if (resourceNode == null)
{
throw new FormatException("The provided raw resource could not be parsed into a valid FHIR resource.");
}

ValidateResourceId(resourceNode.Id);
CheckConditionalReferenceInResource(resourceNode, importMode);

if (resourceNode.Meta == null)
{
resourceNode.Meta = new Meta();
}

var lastUpdatedIsNull = importMode == ImportMode.InitialLoad || resourceNode.Meta.LastUpdated == null;
var lastUpdated = lastUpdatedIsNull ? Clock.UtcNow : resourceNode.Meta.LastUpdated.Value;
resourceNode.Meta.LastUpdated = new DateTimeOffset(lastUpdated.DateTime.TruncateToMillisecond(), lastUpdated.Offset);
Copilot is powered by AI and may make mistakes. Always verify output.
Comment on lines +58 to +65
if (_compiledDelegate != null)
{
results = _compiledDelegate(ignixaElement, ignixaContext);
}
else
{
results = _evaluator.Evaluate(ignixaElement, _ast, ignixaContext);
}

Check notice

Code scanning / CodeQL

Missed ternary opportunity

Both branches of this 'if' statement write to the same variable - consider using '?' to express intent better.

Copilot Autofix

AI 4 days ago

In general, to fix this pattern you replace an if/else statement that only assigns or returns a value with a single statement using the conditional (? :) operator. The condition from the if becomes the ternary condition, and the right-hand sides of the assignments from the if and else blocks become the true and false operands of the ternary.

For this specific case in src/Microsoft.Health.Fhir.Ignixa/FhirPath/IgnixaCompiledFhirPath.cs, method Evaluate, we can remove the separate declaration and later conditional assignment of results, and instead declare and initialize results in one statement using the ternary operator. That is, replace:

IEnumerable<IElement> results;
if (_compiledDelegate != null)
{
    results = _compiledDelegate(ignixaElement, ignixaContext);
}
else
{
    results = _evaluator.Evaluate(ignixaElement, _ast, ignixaContext);
}

with:

IEnumerable<IElement> results = _compiledDelegate != null
    ? _compiledDelegate(ignixaElement, ignixaContext)
    : _evaluator.Evaluate(ignixaElement, _ast, ignixaContext);

This keeps behavior identical, does not require any new imports or definitions, and is fully contained within the shown method. No other parts of the file need to be changed.

Suggested changeset 1
src/Microsoft.Health.Fhir.Ignixa/FhirPath/IgnixaCompiledFhirPath.cs

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/src/Microsoft.Health.Fhir.Ignixa/FhirPath/IgnixaCompiledFhirPath.cs b/src/Microsoft.Health.Fhir.Ignixa/FhirPath/IgnixaCompiledFhirPath.cs
--- a/src/Microsoft.Health.Fhir.Ignixa/FhirPath/IgnixaCompiledFhirPath.cs
+++ b/src/Microsoft.Health.Fhir.Ignixa/FhirPath/IgnixaCompiledFhirPath.cs
@@ -54,15 +54,9 @@
         var ignixaContext = CreateIgnixaContext(ignixaElement, context);
 
         // Evaluate using compiled delegate (fast path) or interpreter (fallback)
-        IEnumerable<IElement> results;
-        if (_compiledDelegate != null)
-        {
-            results = _compiledDelegate(ignixaElement, ignixaContext);
-        }
-        else
-        {
-            results = _evaluator.Evaluate(ignixaElement, _ast, ignixaContext);
-        }
+        IEnumerable<IElement> results = _compiledDelegate != null
+            ? _compiledDelegate(ignixaElement, ignixaContext)
+            : _evaluator.Evaluate(ignixaElement, _ast, ignixaContext);
 
         // Convert results back to ITypedElement
         foreach (var result in results)
EOF
@@ -54,15 +54,9 @@
var ignixaContext = CreateIgnixaContext(ignixaElement, context);

// Evaluate using compiled delegate (fast path) or interpreter (fallback)
IEnumerable<IElement> results;
if (_compiledDelegate != null)
{
results = _compiledDelegate(ignixaElement, ignixaContext);
}
else
{
results = _evaluator.Evaluate(ignixaElement, _ast, ignixaContext);
}
IEnumerable<IElement> results = _compiledDelegate != null
? _compiledDelegate(ignixaElement, ignixaContext)
: _evaluator.Evaluate(ignixaElement, _ast, ignixaContext);

// Convert results back to ITypedElement
foreach (var result in results)
Copilot is powered by AI and may make mistakes. Always verify output.
Unable to commit as this autofix suggestion is now outdated
Comment on lines +87 to +101
foreach (var field in referenceMetadata)
{
if (string.IsNullOrWhiteSpace(reference.Reference))
// Strip [x] suffix for choice types - FhirPath handles polymorphic fields
var elementPath = field.ElementPath.EndsWith("[x]", StringComparison.Ordinal)
? field.ElementPath.Substring(0, field.ElementPath.Length - 3)
: field.ElementPath;

// Use FhirPath to check if any reference contains '?' (conditional reference)
// FhirPath naturally handles collections and nested paths
var fhirPath = $"{elementPath}.reference.contains('?')";
if (ignixaElement.Predicate(fhirPath))
{
continue;
throw new NotSupportedException($"Conditional reference is not supported for $import in {ImportMode.InitialLoad}.");
}
}

Check notice

Code scanning / CodeQL

Missed opportunity to use Select

This foreach loop immediately [maps its iteration variable to another variable](1) - consider mapping the sequence explicitly using '.Select(...)'.

Copilot Autofix

AI 4 days ago

In general, to fix this kind of issue you project the original sequence into the value you actually want to iterate over using LINQ’s Select, and iterate over that projection instead of computing the mapped value inside the loop body.

Here, referenceMetadata is only used to derive elementPath via:

var elementPath = field.ElementPath.EndsWith("[x]", StringComparison.Ordinal)
    ? field.ElementPath.Substring(0, field.ElementPath.Length - 3)
    : field.ElementPath;

and only elementPath is used afterward. We can therefore replace:

foreach (var field in referenceMetadata)
{
    var elementPath = /* mapping from field */;
    ...
}

with:

foreach (var elementPath in referenceMetadata.Select(field =>
    field.ElementPath.EndsWith("[x]", StringComparison.Ordinal)
        ? field.ElementPath.Substring(0, field.ElementPath.Length - 3)
        : field.ElementPath))
{
    ...
}

This keeps the loop body unchanged except that it now refers directly to elementPath from the projection. To support Select, we must add using System.Linq; at the top of ImportResourceParser.cs. No other behavior changes are introduced: the mapping logic and subsequent FhirPath predicate evaluation remain identical.

Suggested changeset 1
src/Microsoft.Health.Fhir.Shared.Core/Features/Operations/Import/ImportResourceParser.cs

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/src/Microsoft.Health.Fhir.Shared.Core/Features/Operations/Import/ImportResourceParser.cs b/src/Microsoft.Health.Fhir.Shared.Core/Features/Operations/Import/ImportResourceParser.cs
--- a/src/Microsoft.Health.Fhir.Shared.Core/Features/Operations/Import/ImportResourceParser.cs
+++ b/src/Microsoft.Health.Fhir.Shared.Core/Features/Operations/Import/ImportResourceParser.cs
@@ -4,6 +4,7 @@
 // -------------------------------------------------------------------------------------------------
 
 using System;
+using System.Linq;
 using System.Text.Json.Nodes;
 using System.Text.RegularExpressions;
 using EnsureThat;
@@ -84,13 +85,11 @@
 
             // Use IReferenceMetadataProvider to identify reference fields for this resource type
             var referenceMetadata = _schemaContext.ReferenceMetadataProvider.GetMetadata(resource.ResourceType);
-            foreach (var field in referenceMetadata)
-            {
-                // Strip [x] suffix for choice types - FhirPath handles polymorphic fields
-                var elementPath = field.ElementPath.EndsWith("[x]", StringComparison.Ordinal)
+            foreach (var elementPath in referenceMetadata.Select(field =>
+                field.ElementPath.EndsWith("[x]", StringComparison.Ordinal)
                     ? field.ElementPath.Substring(0, field.ElementPath.Length - 3)
-                    : field.ElementPath;
-
+                    : field.ElementPath))
+            {
                 // Use FhirPath to check if any reference contains '?' (conditional reference)
                 // FhirPath naturally handles collections and nested paths
                 var fhirPath = $"{elementPath}.reference.contains('?')";
EOF
@@ -4,6 +4,7 @@
// -------------------------------------------------------------------------------------------------

using System;
using System.Linq;
using System.Text.Json.Nodes;
using System.Text.RegularExpressions;
using EnsureThat;
@@ -84,13 +85,11 @@

// Use IReferenceMetadataProvider to identify reference fields for this resource type
var referenceMetadata = _schemaContext.ReferenceMetadataProvider.GetMetadata(resource.ResourceType);
foreach (var field in referenceMetadata)
{
// Strip [x] suffix for choice types - FhirPath handles polymorphic fields
var elementPath = field.ElementPath.EndsWith("[x]", StringComparison.Ordinal)
foreach (var elementPath in referenceMetadata.Select(field =>
field.ElementPath.EndsWith("[x]", StringComparison.Ordinal)
? field.ElementPath.Substring(0, field.ElementPath.Length - 3)
: field.ElementPath;

: field.ElementPath))
{
// Use FhirPath to check if any reference contains '?' (conditional reference)
// FhirPath naturally handles collections and nested paths
var fhirPath = $"{elementPath}.reference.contains('?')";
Copilot is powered by AI and may make mistakes. Always verify output.
Comment on lines +122 to +132
foreach (var issue in result.Issues)
{
if (issue.Severity == IssueSeverity.Error || issue.Severity == IssueSeverity.Fatal)
{
var memberNames = string.IsNullOrEmpty(issue.Path)
? null
: new[] { issue.Path };

validationResults.Add(new DataAnnotations.ValidationResult(issue.Message, memberNames));
}
}

Check notice

Code scanning / CodeQL

Missed opportunity to use Where

This foreach loop [implicitly filters its target sequence](1) - consider filtering the sequence explicitly using '.Where(...)'.

Copilot Autofix

AI 4 days ago

To fix this, we should explicitly filter result.Issues before iterating, using LINQ’s Where extension method with the existing condition (issue.Severity == IssueSeverity.Error || issue.Severity == IssueSeverity.Fatal). This removes the internal if and ensures the loop only runs for relevant issues.

Concretely, in TryValidateIgnixa in IgnixaResourceValidator.cs, replace:

foreach (var issue in result.Issues)
{
    if (issue.Severity == IssueSeverity.Error || issue.Severity == IssueSeverity.Fatal)
    {
        var memberNames = string.IsNullOrEmpty(issue.Path)
            ? null
            : new[] { issue.Path };

        validationResults.Add(new DataAnnotations.ValidationResult(issue.Message, memberNames));
    }
}

with:

foreach (var issue in result.Issues.Where(issue =>
             issue.Severity == IssueSeverity.Error || issue.Severity == IssueSeverity.Fatal))
{
    var memberNames = string.IsNullOrEmpty(issue.Path)
        ? null
        : new[] { issue.Path };

    validationResults.Add(new DataAnnotations.ValidationResult(issue.Message, memberNames));
}

This keeps all functionality identical—only error and fatal severities produce ValidationResult entries; memberNames logic is unchanged; and validationResults is still only used when non-null. To compile, we need to ensure System.Linq is imported at the top of the file, since we are now using Where as an extension method. No other methods or definitions are required.

Suggested changeset 1
src/Microsoft.Health.Fhir.Shared.Core/Features/Validation/IgnixaResourceValidator.cs

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/src/Microsoft.Health.Fhir.Shared.Core/Features/Validation/IgnixaResourceValidator.cs b/src/Microsoft.Health.Fhir.Shared.Core/Features/Validation/IgnixaResourceValidator.cs
--- a/src/Microsoft.Health.Fhir.Shared.Core/Features/Validation/IgnixaResourceValidator.cs
+++ b/src/Microsoft.Health.Fhir.Shared.Core/Features/Validation/IgnixaResourceValidator.cs
@@ -6,6 +6,7 @@
 using System;
 using System.Collections.Concurrent;
 using System.Collections.Generic;
+using System.Linq;
 using EnsureThat;
 using Ignixa.Abstractions;
 using Ignixa.Serialization.SourceNodes;
@@ -119,16 +120,14 @@
             // Convert issues to ValidationResult if collection is provided
             if (validationResults != null)
             {
-                foreach (var issue in result.Issues)
+                foreach (var issue in result.Issues.Where(issue =>
+                             issue.Severity == IssueSeverity.Error || issue.Severity == IssueSeverity.Fatal))
                 {
-                    if (issue.Severity == IssueSeverity.Error || issue.Severity == IssueSeverity.Fatal)
-                    {
-                        var memberNames = string.IsNullOrEmpty(issue.Path)
-                            ? null
-                            : new[] { issue.Path };
+                    var memberNames = string.IsNullOrEmpty(issue.Path)
+                        ? null
+                        : new[] { issue.Path };
 
-                        validationResults.Add(new DataAnnotations.ValidationResult(issue.Message, memberNames));
-                    }
+                    validationResults.Add(new DataAnnotations.ValidationResult(issue.Message, memberNames));
                 }
             }
 
EOF
@@ -6,6 +6,7 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using EnsureThat;
using Ignixa.Abstractions;
using Ignixa.Serialization.SourceNodes;
@@ -119,16 +120,14 @@
// Convert issues to ValidationResult if collection is provided
if (validationResults != null)
{
foreach (var issue in result.Issues)
foreach (var issue in result.Issues.Where(issue =>
issue.Severity == IssueSeverity.Error || issue.Severity == IssueSeverity.Fatal))
{
if (issue.Severity == IssueSeverity.Error || issue.Severity == IssueSeverity.Fatal)
{
var memberNames = string.IsNullOrEmpty(issue.Path)
? null
: new[] { issue.Path };
var memberNames = string.IsNullOrEmpty(issue.Path)
? null
: new[] { issue.Path };

validationResults.Add(new DataAnnotations.ValidationResult(issue.Message, memberNames));
}
validationResults.Add(new DataAnnotations.ValidationResult(issue.Message, memberNames));
}
}

Copilot is powered by AI and may make mistakes. Always verify output.
Unable to commit as this autofix suggestion is now outdated
@brendankowitz brendankowitz force-pushed the personal/bkowitz/ignixa-sdk branch from 278c241 to dee7175 Compare January 9, 2026 23:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants