Skip to content

Commit 507e511

Browse files
authored
Merge pull request #3224 from FirelyTeam/copilot/fix-3171
Fix NullReferenceException in primitive types GetHashCode() when Value is null
2 parents 1afe78b + 3deaa9f commit 507e511

File tree

6 files changed

+151
-6
lines changed

6 files changed

+151
-6
lines changed

src/Hl7.Fhir.Base/Model/Date-comparators.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,6 @@ public override bool Equals(object obj)
110110
return false;
111111
}
112112

113-
public override int GetHashCode() => Value.GetHashCode();
113+
public override int GetHashCode() => Value?.GetHashCode() ?? 0;
114114
}
115115
}

src/Hl7.Fhir.Base/Model/FhirDateTime-comparators.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,6 @@ public override bool Equals(object obj)
109109
return false;
110110
}
111111

112-
public override int GetHashCode() => Value.GetHashCode();
112+
public override int GetHashCode() => Value?.GetHashCode() ?? 0;
113113
}
114114
}

src/Hl7.Fhir.Base/Model/Instant-comparators.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,6 @@ public override bool Equals(object obj)
115115
return false;
116116
}
117117

118-
public override int GetHashCode() => Value.GetHashCode();
118+
public override int GetHashCode() => Value?.GetHashCode() ?? 0;
119119
}
120120
}

src/Hl7.Fhir.Base/Model/Time-comparators.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,6 @@ public override bool Equals(object obj)
118118
return false;
119119
}
120120

121-
public override int GetHashCode() => Value.GetHashCode();
121+
public override int GetHashCode() => Value?.GetHashCode() ?? 0;
122122
}
123123
}

src/Hl7.Fhir.R4.Tests/Model/ModelTests.cs

Lines changed: 97 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,11 @@
99
using FluentAssertions;
1010
using Hl7.Fhir.Model;
1111
using Hl7.Fhir.Utility;
12+
using Hl7.Fhir.Validation;
1213
using Microsoft.VisualStudio.TestTools.UnitTesting;
14+
using System.Collections.Generic;
15+
using System.ComponentModel.DataAnnotations;
16+
using System;
1317

1418
namespace Hl7.Fhir.Tests.Model
1519
{
@@ -48,12 +52,103 @@ public void TestCheckMinorVersionCompatibiliy()
4852
Assert.IsFalse(ModelInfo.CheckMinorVersionCompatibility("3"));
4953
}
5054

51-
//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".
55+
//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".
5256
[TestMethod]
5357
public void TestCorrectCurrencyDescription()
5458
{
5559
var currency = Money.Currencies.STN;
56-
currency.GetDocumentation().Should().Be("São Tomé and Príncipe dobra");
60+
currency.GetDocumentation().Should().Be("São Tomé and Príncipe dobra");
61+
}
62+
63+
[TestMethod]
64+
public void ValidatePatientWithDataAbsentExtension()
65+
{
66+
// Test for issue #3171 - Patient.Validate(true) throws NullReferenceException
67+
// when BirthDate has data-absent-reason extension but no value
68+
// This reproduces the exact scenario from the original issue #3171 report
69+
70+
var patient = new Patient()
71+
{
72+
BirthDateElement = new Date()
73+
{
74+
Extension = new List<Extension>()
75+
{
76+
new Extension
77+
{
78+
Url = "http://hl7.org/fhir/StructureDefinition/data-absent-reason",
79+
Value = new Code
80+
{
81+
Value = "unknown"
82+
}
83+
}
84+
}
85+
}
86+
};
87+
88+
// This exact line was failing with "Object reference not set to an instance of an object"
89+
// in netstandard2.0 and earlier .NET versions, due to GetHashCode() being called
90+
// on primitive types with null values during validation
91+
patient.Validate(true); // Should not throw NullReferenceException anymore
92+
93+
// Ensure patient.Validate(false) still works as it did before
94+
patient.Validate(false); // This was working before the fix
95+
96+
// Also test with TryValidate to ensure both validation paths work
97+
ICollection<ValidationResult> results = new List<ValidationResult>();
98+
bool isValid = DotNetAttributeValidation.TryValidate(patient, results, true);
99+
// The validation may or may not pass (depends on other validation rules),
100+
// but it should not throw an exception
101+
}
102+
103+
[TestMethod]
104+
public void DateGetHashCodeWithNullValue()
105+
{
106+
// Direct test for Date.GetHashCode() with null value - reproduces issue #3171
107+
var date = new Date();
108+
// Verify that Value is null
109+
Assert.IsNull(date.Value);
110+
111+
// This should not throw NullReferenceException
112+
try
113+
{
114+
int hashCode = date.GetHashCode();
115+
Assert.IsTrue(true, "GetHashCode completed without throwing an exception");
116+
}
117+
catch (NullReferenceException ex)
118+
{
119+
Assert.Fail($"GetHashCode threw NullReferenceException: {ex.Message}");
120+
}
121+
}
122+
123+
[TestMethod]
124+
public void AllPrimitiveTypesGetHashCodeWithNullValue()
125+
{
126+
// Test all primitive types to ensure they handle null values correctly
127+
var date = new Date();
128+
var dateTime = new FhirDateTime();
129+
var instant = new Instant();
130+
var time = new Time();
131+
132+
// All should have null values
133+
Assert.IsNull(date.Value);
134+
Assert.IsNull(dateTime.Value);
135+
Assert.IsNull(instant.Value);
136+
Assert.IsNull(time.Value);
137+
138+
// None should throw exceptions when GetHashCode is called
139+
try
140+
{
141+
int hashCode1 = date.GetHashCode();
142+
int hashCode2 = dateTime.GetHashCode();
143+
int hashCode3 = instant.GetHashCode();
144+
int hashCode4 = time.GetHashCode();
145+
146+
Assert.IsTrue(true, "All GetHashCode calls completed without throwing exceptions");
147+
}
148+
catch (NullReferenceException ex)
149+
{
150+
Assert.Fail($"One of the GetHashCode calls threw NullReferenceException: {ex.Message}");
151+
}
57152
}
58153
}
59154
}

src/Hl7.Fhir.Shared.Tests/Validation/ValidatePatient.cs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,5 +47,55 @@ public void ValidateDemoPatient()
4747
Assert.IsFalse(DotNetAttributeValidation.TryValidate(patient, results, true));
4848
Assert.IsTrue(results.Count > 0);
4949
}
50+
51+
[TestMethod]
52+
public void ValidatePatientWithDataAbsentExtension()
53+
{
54+
// Test for issue #3171 - Patient.Validate(true) throws NullReferenceException
55+
// when BirthDate has data-absent-reason extension but no value
56+
var patient = new Patient()
57+
{
58+
BirthDateElement = new Date()
59+
{
60+
Extension = new List<Extension>()
61+
{
62+
new Extension
63+
{
64+
Url = "http://hl7.org/fhir/StructureDefinition/data-absent-reason",
65+
Value = new Code
66+
{
67+
Value = "unknown"
68+
}
69+
}
70+
}
71+
}
72+
};
73+
74+
// This should not throw an exception
75+
try
76+
{
77+
patient.Validate(true);
78+
// If we get here, the validation succeeded without throwing an exception
79+
Assert.IsTrue(true, "Validation completed without throwing an exception");
80+
}
81+
catch (System.NullReferenceException ex)
82+
{
83+
Assert.Fail($"Validation threw NullReferenceException: {ex.Message}");
84+
}
85+
86+
// Also test with TryValidate
87+
ICollection<ValidationResult> results = new List<ValidationResult>();
88+
try
89+
{
90+
bool isValid = DotNetAttributeValidation.TryValidate(patient, results, true);
91+
// The validation may or may not pass (depends on other validation rules),
92+
// but it should not throw an exception
93+
Assert.IsTrue(true, "TryValidate completed without throwing an exception");
94+
}
95+
catch (System.NullReferenceException ex)
96+
{
97+
Assert.Fail($"TryValidate threw NullReferenceException: {ex.Message}");
98+
}
99+
}
50100
}
51101
}

0 commit comments

Comments
 (0)