Skip to content

Fix NullReferenceException in primitive types GetHashCode() when Value is null #3224

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

Merged
merged 5 commits into from
Jul 24, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/Hl7.Fhir.Base/Model/Date-comparators.cs
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,6 @@ public override bool Equals(object obj)
return false;
}

public override int GetHashCode() => Value.GetHashCode();
public override int GetHashCode() => Value?.GetHashCode() ?? 0;
}
}
2 changes: 1 addition & 1 deletion src/Hl7.Fhir.Base/Model/FhirDateTime-comparators.cs
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,6 @@ public override bool Equals(object obj)
return false;
}

public override int GetHashCode() => Value.GetHashCode();
public override int GetHashCode() => Value?.GetHashCode() ?? 0;
}
}
2 changes: 1 addition & 1 deletion src/Hl7.Fhir.Base/Model/Instant-comparators.cs
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,6 @@ public override bool Equals(object obj)
return false;
}

public override int GetHashCode() => Value.GetHashCode();
public override int GetHashCode() => Value?.GetHashCode() ?? 0;
}
}
2 changes: 1 addition & 1 deletion src/Hl7.Fhir.Base/Model/Time-comparators.cs
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,6 @@ public override bool Equals(object obj)
return false;
}

public override int GetHashCode() => Value.GetHashCode();
public override int GetHashCode() => Value?.GetHashCode() ?? 0;
}
}
99 changes: 97 additions & 2 deletions src/Hl7.Fhir.R4.Tests/Model/ModelTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@
using FluentAssertions;
using Hl7.Fhir.Model;
using Hl7.Fhir.Utility;
using Hl7.Fhir.Validation;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System;

namespace Hl7.Fhir.Tests.Model
{
Expand Down Expand Up @@ -48,12 +52,103 @@ public void TestCheckMinorVersionCompatibiliy()
Assert.IsFalse(ModelInfo.CheckMinorVersionCompatibility("3"));
}

//If failed: change the description of the "STN" in the Currency enum of Money.cs from "SC#o TomC) and PrC-ncipe dobra" to "S�o Tom� and Pr�ncipe dobra".
//If failed: change the description of the "STN" in the Currency enum of Money.cs from "SC#o TomC) and PrC-ncipe dobra" to "São Tomé and Príncipe dobra".
[TestMethod]
public void TestCorrectCurrencyDescription()
{
var currency = Money.Currencies.STN;
currency.GetDocumentation().Should().Be("S�o Tom� and Pr�ncipe dobra");
currency.GetDocumentation().Should().Be("São Tomé and Príncipe dobra");
}

[TestMethod]
public void ValidatePatientWithDataAbsentExtension()
{
// Test for issue #3171 - Patient.Validate(true) throws NullReferenceException
// when BirthDate has data-absent-reason extension but no value
// This reproduces the exact scenario from the original issue #3171 report

var patient = new Patient()
{
BirthDateElement = new Date()
{
Extension = new List<Extension>()
{
new Extension
{
Url = "http://hl7.org/fhir/StructureDefinition/data-absent-reason",
Value = new Code
{
Value = "unknown"
}
}
}
}
};

// This exact line was failing with "Object reference not set to an instance of an object"
// in netstandard2.0 and earlier .NET versions, due to GetHashCode() being called
// on primitive types with null values during validation
patient.Validate(true); // Should not throw NullReferenceException anymore

// Ensure patient.Validate(false) still works as it did before
patient.Validate(false); // This was working before the fix

// Also test with TryValidate to ensure both validation paths work
ICollection<ValidationResult> results = new List<ValidationResult>();
bool isValid = DotNetAttributeValidation.TryValidate(patient, results, true);
// The validation may or may not pass (depends on other validation rules),
// but it should not throw an exception
}

[TestMethod]
public void DateGetHashCodeWithNullValue()
{
// Direct test for Date.GetHashCode() with null value - reproduces issue #3171
var date = new Date();
// Verify that Value is null
Assert.IsNull(date.Value);

// This should not throw NullReferenceException
try
{
int hashCode = date.GetHashCode();
Assert.IsTrue(true, "GetHashCode completed without throwing an exception");
}
catch (NullReferenceException ex)
{
Assert.Fail($"GetHashCode threw NullReferenceException: {ex.Message}");
}
}

[TestMethod]
public void AllPrimitiveTypesGetHashCodeWithNullValue()
{
// Test all primitive types to ensure they handle null values correctly
var date = new Date();
var dateTime = new FhirDateTime();
var instant = new Instant();
var time = new Time();

// All should have null values
Assert.IsNull(date.Value);
Assert.IsNull(dateTime.Value);
Assert.IsNull(instant.Value);
Assert.IsNull(time.Value);

// None should throw exceptions when GetHashCode is called
try
{
int hashCode1 = date.GetHashCode();
int hashCode2 = dateTime.GetHashCode();
int hashCode3 = instant.GetHashCode();
int hashCode4 = time.GetHashCode();

Assert.IsTrue(true, "All GetHashCode calls completed without throwing exceptions");
}
catch (NullReferenceException ex)
{
Assert.Fail($"One of the GetHashCode calls threw NullReferenceException: {ex.Message}");
}
}
}
}
50 changes: 50 additions & 0 deletions src/Hl7.Fhir.Shared.Tests/Validation/ValidatePatient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,5 +47,55 @@ public void ValidateDemoPatient()
Assert.IsFalse(DotNetAttributeValidation.TryValidate(patient, results, true));
Assert.IsTrue(results.Count > 0);
}

[TestMethod]
public void ValidatePatientWithDataAbsentExtension()
{
// Test for issue #3171 - Patient.Validate(true) throws NullReferenceException
// when BirthDate has data-absent-reason extension but no value
var patient = new Patient()
{
BirthDateElement = new Date()
{
Extension = new List<Extension>()
{
new Extension
{
Url = "http://hl7.org/fhir/StructureDefinition/data-absent-reason",
Value = new Code
{
Value = "unknown"
}
}
}
}
};

// This should not throw an exception
try
{
patient.Validate(true);
// If we get here, the validation succeeded without throwing an exception
Assert.IsTrue(true, "Validation completed without throwing an exception");
}
catch (System.NullReferenceException ex)
{
Assert.Fail($"Validation threw NullReferenceException: {ex.Message}");
}

// Also test with TryValidate
ICollection<ValidationResult> results = new List<ValidationResult>();
try
{
bool isValid = DotNetAttributeValidation.TryValidate(patient, results, true);
// The validation may or may not pass (depends on other validation rules),
// but it should not throw an exception
Assert.IsTrue(true, "TryValidate completed without throwing an exception");
}
catch (System.NullReferenceException ex)
{
Assert.Fail($"TryValidate threw NullReferenceException: {ex.Message}");
}
}
}
}