-
Notifications
You must be signed in to change notification settings - Fork 574
[Not for review, prototype] Personal/bkowitz/ignixa sdk #5306
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
- 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>
5c61cca to
67011b0
Compare
| // Try conversion for common types | ||
| if (value != null && typeof(T) == typeof(string)) | ||
| { | ||
| return (T)(object)value.ToString(); |
Check warning
Code scanning / CodeQL
Useless upcast
Show autofix suggestion
Hide autofix suggestion
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.
-
Copy modified line R64
| @@ -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; |
| // Try conversion for common types | ||
| if (value != null && typeof(T) == typeof(string)) | ||
| { | ||
| return (T)(object)value.ToString()!; |
Check warning
Code scanning / CodeQL
Useless upcast
Show autofix suggestion
Hide autofix suggestion
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.
-
Copy modified line R95
| @@ -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; |
| 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
Show autofix suggestion
Hide autofix suggestion
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.
-
Copy modified lines R42-R46
| @@ -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; |
|
|
||
| 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
Show autofix suggestion
Hide autofix suggestion
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
resourceNodeis null, throw an informativeFormatExceptionorArgumentExceptionindicating that the raw resource is invalid. - Before accessing
resourceNode.Meta, ensure it is non-null. If the FHIR node type permitsMetato 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.
-
Copy modified lines R42-R48 -
Copy modified lines R51-R55
| @@ -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); |
| if (_compiledDelegate != null) | ||
| { | ||
| results = _compiledDelegate(ignixaElement, ignixaContext); | ||
| } | ||
| else | ||
| { | ||
| results = _evaluator.Evaluate(ignixaElement, _ast, ignixaContext); | ||
| } |
Check notice
Code scanning / CodeQL
Missed ternary opportunity
Show autofix suggestion
Hide autofix suggestion
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.
-
Copy modified lines R57-R59
| @@ -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) |
| 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
Show autofix suggestion
Hide autofix suggestion
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.
-
Copy modified line R7 -
Copy modified lines R88-R89 -
Copy modified lines R91-R92
| @@ -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('?')"; |
| 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
Show autofix suggestion
Hide autofix suggestion
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.
-
Copy modified line R9 -
Copy modified lines R123-R124 -
Copy modified lines R126-R128 -
Copy modified line R130
| @@ -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)); | ||
| } | ||
| } | ||
|
|
278c241 to
dee7175
Compare
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:
net9.0the only target framework inDirectory.Build.propsto align with Ignixa's requirements.Directory.Packages.propsto resolve compatibility issues with Ignixa shims.Ignixa.Abstractions,Ignixa.Serialization,Ignixa.Extensions.FirelySdk5, etc.) to the solution for FHIR resource handling, serialization, and validation.Build and dependency management:
AspNetPackageVersionset to 9.0.3).R4.slnf. [1] [2]Documentation and migration planning:
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
Semver Change (docs)
Patch|Skip|Feature|Breaking (reason)