From 5e9576191dca8b7cd2ae0efc09b82cf4f6c037df Mon Sep 17 00:00:00 2001 From: Brian Postlethwaite Date: Tue, 14 Jan 2025 12:29:15 +1100 Subject: [PATCH] #3008 While validating an element, use it's FHIR attribute name not the class property name if it is available. (and update unit tests) --- .../DataAnnotationDeserialzationValidator.cs | 9 ++++++++- .../Validation/CodedValidationException.cs | 18 +++++++++++++++++- .../Validation/ValidatePatient.cs | 4 ++++ .../Validation/ValidationTests.cs | 11 +++++++---- 4 files changed, 36 insertions(+), 6 deletions(-) diff --git a/src/Hl7.Fhir.Base/Serialization/DataAnnotationDeserialzationValidator.cs b/src/Hl7.Fhir.Base/Serialization/DataAnnotationDeserialzationValidator.cs index 636dee69ea..c6a9ecefbc 100644 --- a/src/Hl7.Fhir.Base/Serialization/DataAnnotationDeserialzationValidator.cs +++ b/src/Hl7.Fhir.Base/Serialization/DataAnnotationDeserialzationValidator.cs @@ -15,6 +15,7 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; +using System.Reflection; #pragma warning disable CS1580 // Invalid type for parameter in XML comment cref attribute #pragma warning disable CS1584 // XML comment has syntactically incorrect cref attribute @@ -88,7 +89,13 @@ public void ValidateInstance(object? instance, in InstanceDeserializationContext { // Add the name of the property to the path, so we can display the correct name of the element, // even if it does not really contain any values. - var nestedContext = validationContext.IntoEmptyProperty(propMapping.Name); + var useName = propMapping.Name; + // Use the fhir property name, not the .NET property name. + var at = propMapping.NativeProperty.GetCustomAttribute(); + if (at != null) + useName = at.Name; + + var nestedContext = validationContext.IntoEmptyProperty(useName); errors = add(errors, runAttributeValidation(propValue, new[] { cardinality }, nestedContext)); } diff --git a/src/Hl7.Fhir.Base/Validation/CodedValidationException.cs b/src/Hl7.Fhir.Base/Validation/CodedValidationException.cs index 2262ef3b1d..8c60a9d523 100644 --- a/src/Hl7.Fhir.Base/Validation/CodedValidationException.cs +++ b/src/Hl7.Fhir.Base/Validation/CodedValidationException.cs @@ -6,9 +6,11 @@ * available at https://raw.githubusercontent.com/FirelyTeam/firely-net-sdk/master/LICENSE */ +using Hl7.Fhir.Introspection; using Hl7.Fhir.Model; using Hl7.Fhir.Utility; using System.ComponentModel.DataAnnotations; +using System.Reflection; using COVE = Hl7.Fhir.Validation.CodedValidationException; using OO_Sev = Hl7.Fhir.Model.OperationOutcome.IssueSeverity; using OO_Typ = Hl7.Fhir.Model.OperationOutcome.IssueType; @@ -93,7 +95,21 @@ internal static CodedValidationException Initialize(ValidationContext context, s // will return the parent, so we need to add the MemberName. if (context.MemberName is not null) { - path = $"{path}.{context.MemberName}"; + var useName = context.MemberName; + var pm = ReflectionHelper.FindProperty(context.ObjectType, context.MemberName); + if (pm != null) + { + var at = pm.GetCustomAttribute(); + if (at != null) + { + useName = at.Name; + if (at.IsPrimitiveValue) + useName = null; + } + } + + if (!string.IsNullOrEmpty(useName)) + path = $"{path}.{useName}"; } } diff --git a/src/Hl7.Fhir.Shared.Tests/Validation/ValidatePatient.cs b/src/Hl7.Fhir.Shared.Tests/Validation/ValidatePatient.cs index 37ee920056..1848a665b5 100644 --- a/src/Hl7.Fhir.Shared.Tests/Validation/ValidatePatient.cs +++ b/src/Hl7.Fhir.Shared.Tests/Validation/ValidatePatient.cs @@ -10,9 +10,11 @@ using Hl7.Fhir.Serialization; using Hl7.Fhir.Validation; using Microsoft.VisualStudio.TestTools.UnitTesting; +using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.IO; +using System.Linq; using System.Xml; namespace Hl7.Fhir.Tests.Validation @@ -33,6 +35,7 @@ public void ValidateDemoPatient() Assert.IsFalse(DotNetAttributeValidation.TryValidate(patient, results, true)); Assert.IsTrue(results.Count > 0); + Console.WriteLine(string.Join("\n", results.Select(r => r.ErrorMessage))); results.Clear(); foreach (DomainResource contained in patient.Contained) contained.Text = null; @@ -46,6 +49,7 @@ public void ValidateDemoPatient() Assert.IsFalse(DotNetAttributeValidation.TryValidate(patient, results, true)); Assert.IsTrue(results.Count > 0); + Console.WriteLine(string.Join("\n", results.Select(r => r.ErrorMessage))); } } } diff --git a/src/Hl7.Fhir.Shared.Tests/Validation/ValidationTests.cs b/src/Hl7.Fhir.Shared.Tests/Validation/ValidationTests.cs index 915222f41b..bf9faa7033 100644 --- a/src/Hl7.Fhir.Shared.Tests/Validation/ValidationTests.cs +++ b/src/Hl7.Fhir.Shared.Tests/Validation/ValidationTests.cs @@ -36,7 +36,7 @@ public void TestIdValidation() validateErrorOrFail(id); id = new Id("123456789012345678901234567890123456745290123456745290123456745290123456745290"); - validateErrorOrFail(id); + validateErrorOrFail(id, errorMessage: "'123456789012345678901234567890123456745290123456745290123456745290123456745290' is not a correct literal for an id. At Id"); } [TestMethod] @@ -63,7 +63,7 @@ public void ValidatesResourceTag() Assert.IsFalse(DotNetAttributeValidation.TryValidate(p, recurse: true)); } - private static void validateErrorOrFail(Base instance, bool recurse = false, string membername = null) + private static void validateErrorOrFail(Base instance, bool recurse = false, string membername = null, string errorMessage = null) { try { @@ -73,8 +73,11 @@ private static void validateErrorOrFail(Base instance, bool recurse = false, str } catch (ValidationException ve) { + Console.WriteLine($"Error: {ve.ValidationResult?.ErrorMessage}"); if (membername != null) Assert.IsTrue(ve.ValidationResult.MemberNames.Contains(membername)); + if (errorMessage != null) + Assert.AreEqual(errorMessage, ve.ValidationResult.ErrorMessage); } } @@ -213,7 +216,7 @@ public void ValidateResourceWithIncorrectChildElement() DotNetAttributeValidation.Validate(obs); // When we recurse, this should fail - validateErrorOrFail(obs, true, membername: "Value"); + validateErrorOrFail(obs, true, membername: "Value", errorMessage: "'Ewout Kramer' is not a correct literal for a dateTime. At Observation.effective.start"); } #if !NETSTANDARD1_6 @@ -230,7 +233,7 @@ public void TestXhtmlValidation() validateErrorOrFail(p, true); p.Text.Div = "
Smiley
"; - validateErrorOrFail(p, true); + validateErrorOrFail(p, true, null, "Value is not well-formed Xml adhering to the FHIR schema for Narrative: The 'onmouseover' attribute is not declared. At Patient.text.div"); } #endif