Skip to content

Latest commit

 

History

History
251 lines (190 loc) · 11.6 KB

File metadata and controls

251 lines (190 loc) · 11.6 KB

Technical README for Firely CQL SDK Maintainers

This document contains technical details, assumptions, and implementation details for maintaining the Firely CQL SDK.

Multi-Targeting: .NET 8 and .NET 10

The SDK targets both .NET 8 (LTS) and .NET 10 (LTS) to provide performance benefits from .NET 10 while maintaining compatibility with .NET 8.

Target Framework Configuration

  • Base configuration: cql-base.props sets <TargetFrameworks>net8.0;net10.0</TargetFrameworks>
  • SDK projects: All library projects (Hl7.Cql.*) target both frameworks
  • Tool projects: Executable projects like PackagerCLI target both frameworks
  • Example projects: May target a single framework for simplicity

System.Text.Json Type Discriminator Conflict (.NET 10 Breaking Change)

Problem

.NET 10 introduced a breaking change in System.Text.Json where the type discriminator property name cannot conflict with actual property names in polymorphic type hierarchies. The ELM type system uses "type" as both:

  1. A discriminator for polymorphic serialization (e.g., distinguishing ChoiceTypeSpecifier from NamedTypeSpecifier)
  2. An actual property in some types (e.g., ChoiceTypeSpecifier.type is an array of TypeSpecifier[], TupleElementDefinition.type is a TypeSpecifier)

This causes System.InvalidOperationException in .NET 10 when setting up polymorphism.

Solution: Automatic JsonIgnore Attribute Generation

We implemented a code generation solution that automatically adds [System.Text.Json.Serialization.JsonIgnore] attributes to properties that would conflict with the JSON type discriminator:

XSD to C# Code Generator Enhancement:

  • The XsdCodeGenerator in tools/XsdToCSharpConverter now automatically detects properties named "type" with complex types (objects/arrays)
  • It adds [JsonIgnore] attributes to these properties during code generation
  • This prevents JSON serialization of conflicting properties while preserving XML serialization
  • The generated Elm.g.cs includes these attributes automatically

Example Generated Code:

[System.Xml.Serialization.XmlElementAttribute("type")]
[System.Text.Json.Serialization.JsonIgnore]  // Auto-generated
public TypeSpecifier[] type { get; set; }

Benefits:

  • ✅ Clean solution using standard .NET attributes
  • ✅ No manual modifications to generated code needed
  • ✅ No conditional compilation required
  • ✅ No runtime preprocessing or postprocessing
  • ✅ Works identically on both .NET 8 and .NET 10
  • ✅ Better performance (no JSON tree manipulation)
  • ✅ Type-safe at compile time

Custom JSON Converters: The solution also includes custom JSON converters for handling polymorphic ELM types:

  • PolymorphicObjectJsonConverter<T> - Handles polymorphic single objects
  • PolymorphicArrayJsonConverter<T> - Handles polymorphic arrays with ELM-specific wrapping
  • TopLevelDefinitionConverterFactory - Creates converters for top-level definition arrays
  • PolymorphicTypeResolver - Discovers type hierarchies via [XmlInclude] attributes

See Cql/Elm/Serialization/README.md for complete architecture documentation.

Legacy Format Support

The LibraryJsonSerializer includes preprocessing logic (CorrectLegacyConstructs) to handle legacy ELM JSON formats:

  • Converts legacy "type" arrays to "choice" property for ChoiceTypeSpecifier
  • Removes object-valued "type" properties (handled by resultTypeSpecifier)
  • Supports old-style type discriminators on wrapper objects
  • Ensures backward compatibility with ELM generated by older CQL-to-ELM translators

Implementation Files

  1. tools/XsdToCSharpConverter/XsdCodeGenerator.cs:

    • Detects properties named "type" with complex types
    • Automatically adds [JsonIgnore] attributes during code generation
  2. Cql/Elm/Serialization/LibraryJsonSerializer.cs:

    • Central coordinator for JSON serialization/deserialization
    • Configures custom converters and modifiers
    • Handles legacy format conversion
  3. Cql/Elm/Serialization/PolymorphicObjectJsonConverter.cs:

    • Custom converter for polymorphic objects using "type" discriminators
    • Removes discriminator before deserialization to avoid conflicts
  4. Cql/Elm/Serialization/PolymorphicTypeResolver.cs:

    • Discovers type hierarchies via [XmlInclude] attributes
    • Configures polymorphism options with "type" as discriminator

Framework-Agnostic Behavior

Both .NET 8 and .NET 10 now exhibit identical behavior:

  • Same "type" discriminator used for polymorphism
  • Same JSON format for serialization/deserialization
  • No conditional compilation needed in serialization code
  • Test results are identical across frameworks

Custom Build Targets

Custom MSBuild targets (e.g., ElmTooling.Targets.xml, CqlTooling.Targets.xml) that generate code or artifacts are configured to run only once when multi-targeting:

<!-- Use ToolTargetFramework property to specify the latest framework -->
<PropertyGroup>
    <ToolTargetFramework>net10.0</ToolTargetFramework>
</PropertyGroup>

<!-- Add condition to only run on the tool framework -->
<Target Name="GenerateCSharp"
        Condition="'$(TargetFramework)' == '$(ToolTargetFramework)' Or '$(TargetFramework)' == ''"
        BeforeTargets="PreBuildEvent">

This prevents generating the same artifacts multiple times and ensures tools use the latest framework.

Package References

System.Text.Json:

  • .NET 8: Requires explicit PackageReference
  • .NET 10: Included in the framework, explicit reference causes NU1510 error
  • Solution: Removed explicit PackageReference as it's transitively included from dependencies

Microsoft.CodeAnalysis.CSharp:

  • Used for runtime C# compilation in CodeGeneration.NET
  • Version 4.12.0 provides cross-platform Roslyn compiler APIs

Code Generation Version Management

GeneratorToolVersion is a separate scale from the SDK package version, which uses EffVer — see versioning.md. The generator version does follow SemVer.

When modifying C# code generation logic in CodeGeneration.NET, always update LibrarySetCSharpCodeGenerator.GeneratorToolVersion:

  • The version is in CodeGeneration.NET/_CODE GENERATOR VERSION_.cs
  • Follow semantic versioning (major.minor.patch.build) — this applies to the generator version only, not to the SDK package version
  • Ensure LibraryInstanceInvoker supports the new version range
  • Regenerate libraries after version changes
  • Any change here makes the next SDK release at least MESO, because consumers regenerate

Build and Test

Building

# Build all SDK projects (recommended - excludes submodules)
dotnet build Cql-Sdk.slnf -c Debug

# Build for specific framework
dotnet build Cql-Sdk.slnf -c Debug --framework net10.0

Testing

Local Testing

# Run tests for both frameworks
dotnet test Cql/CoreTests/CoreTests.csproj -c Debug

# Run tests for specific framework
dotnet test Cql/CoreTests/CoreTests.csproj -c Debug --framework net8.0
dotnet test Cql/CoreTests/CoreTests.csproj -c Debug --framework net10.0

CI/CD Multi-Framework Testing

The repository includes build/build-test-sign.yml, a dedicated Azure Pipelines template that builds, tests, and signs assemblies for both .NET 8 and .NET 10 in parallel. This template implements three parallel jobs to maximize efficiency while following Microsoft's best practices for multi-targeting validation.

Configuration: The multi-framework testing is fully integrated and runs automatically on every build in a dedicated stage after the main build stage.

Test Categories:

  1. Multi-Target Tests (tested on both .NET 8 and .NET 10):

    • Cql/CoreTests/CoreTests.csproj - Core SDK tests
    • Cql/CqlToElmTests/CqlToElmTests.csproj - CQL to ELM conversion tests
  2. .NET 10 Only Tests:

    • submodules/Firely.Cql.Sdk.Integration.Runner/IntegrationRunner/IntegrationRunner.csproj
    • Demo/Test.Measures.Demo/Test.Measures.Demo.csproj
  3. Excluded Tests:

    • tools/XsdToCSharpConverterTests/XsdToCSharpConverterTests.csproj - Internal tool tests
    • submodules/Ncqa.DQIC/Ncqa.HT.DeckTests/Ncqa.HT.DeckTests.csproj - Commented out
    • submodules/Ncqa.DQIC/Ncqa.HT.MeasuresTests/Ncqa.HT.MeasuresTests.csproj - Commented out

Benefits:

  • ✅ Tests run against both frameworks simultaneously (parallel execution)
  • ✅ Framework-specific test results and code coverage
  • ✅ Automatic comparison report to identify framework-specific issues
  • ✅ Early detection of framework behavioral differences

Usage: See build/README.md for complete documentation and configuration details.

Local Testing: Use the provided scripts for local validation:

# Windows - Test all multi-target projects
.\test-multiframework.ps1

# Windows - Test specific project
.\test-multiframework.ps1 -TestProject CoreTests
# Linux/macOS - Test all multi-target projects
./test-multiframework.sh

# Linux/macOS - Test specific project
./test-multiframework.sh CoreTests

Cross-Platform Considerations

  • Always maintain both PowerShell (.ps1) and Bash (.sh) script variants
  • Use OS-conditional logic in MSBuild: Condition="'$(OS)' == 'Windows_NT'" for Windows
  • Use correct case for directory paths - Unix filesystems are case-sensitive
  • Don't hardcode path separators - use MSBuild properties
  • PowerShell commands via bash should use -NonInteractive flag

Conditional Compilation Symbols

The SDK minimizes the use of conditional compilation. Standard .NET conditional compilation symbols are available if needed:

  • NET8_0: Code specific to .NET 8
  • NET10_0: Code specific to .NET 10
  • NET10_0_OR_GREATER: Code for .NET 10 and later

Example (rarely needed with current architecture):

#if NET10_0_OR_GREATER
    // .NET 10+ specific code
#else
    // .NET 8 specific code
#endif

Note: The current serialization architecture avoids conditional compilation entirely by using automatic [JsonIgnore] attribute generation and framework-agnostic custom converters.

Future .NET Versions

When adding support for future .NET versions (e.g., .NET 12 LTS):

  1. Update cql-base.props with new target framework
  2. Test System.Text.Json behavior - the [JsonIgnore] solution should continue to work
  3. Review if any new conditional compilation is needed (should be minimal/none)
  4. Update this documentation
  5. Update main README.md
  6. Test all custom build targets run only once
  7. Update CI/CD pipelines (build variables, SDK versions)

Common Issues and Solutions

Issue: Build artifacts generated multiple times

Solution: Ensure custom targets have Condition="'$(TargetFramework)' == 'net10.0' Or '$(TargetFramework)' == ''"

Issue: System.Text.Json serialization fails with "type" property conflicts

Solution: Regenerate Elm.g.cs using the updated XsdCodeGenerator which automatically adds [JsonIgnore] attributes to conflicting properties.

Issue: Tests fail on one framework but pass on another

Solution: Verify both frameworks use identical serialization logic. With current architecture, test results should be identical across frameworks. If not, investigate custom converters or legacy format handling.

Additional Resources