From 4e82cbfb0c8b8c7fb614d807595c73b4c38a5381 Mon Sep 17 00:00:00 2001 From: MagnusNordboe Date: Mon, 23 Feb 2026 09:23:54 +0100 Subject: [PATCH 1/3] implementerer XSD validering --- .../Configuration/AppSettings.cs | 5 + .../Extensions/ServiceCollectionExtensions.cs | 5 + .../Validation/Default/XsdValidator.cs | 120 ++++++++++++++++++ .../Internal/Texts/TranslationService.cs | 11 ++ 4 files changed, 141 insertions(+) create mode 100644 src/Altinn.App.Core/Features/Validation/Default/XsdValidator.cs diff --git a/src/Altinn.App.Core/Configuration/AppSettings.cs b/src/Altinn.App.Core/Configuration/AppSettings.cs index 447bf32b65..f9ab269982 100644 --- a/src/Altinn.App.Core/Configuration/AppSettings.cs +++ b/src/Altinn.App.Core/Configuration/AppSettings.cs @@ -215,6 +215,11 @@ public class AppSettings /// public bool ExpressionValidation { get; set; } + /// + /// Enable the functionality to validate form data against corresponding XSD if present + /// + public bool XsdValidation { get; set; } + /// /// Enables OpenTelemetry as a substitute for Application Insights SDK /// Improves instrumentation throughout the Altinn app libraries. diff --git a/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs b/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs index 0e2781d1a3..6bc83c8b50 100644 --- a/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs +++ b/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs @@ -243,6 +243,11 @@ private static void AddValidationServices(IServiceCollection services, IConfigur { services.AddTransient(); } + + if (appSettings?.XsdValidation is true) + { + services.AddTransient(); + } } /// diff --git a/src/Altinn.App.Core/Features/Validation/Default/XsdValidator.cs b/src/Altinn.App.Core/Features/Validation/Default/XsdValidator.cs new file mode 100644 index 0000000000..ece117e1e7 --- /dev/null +++ b/src/Altinn.App.Core/Features/Validation/Default/XsdValidator.cs @@ -0,0 +1,120 @@ +using System.Xml; +using System.Xml.Schema; +using Altinn.App.Core.Helpers; +using Altinn.App.Core.Helpers.Serialization; +using Altinn.App.Core.Internal.App; +using Altinn.App.Core.Models; +using Altinn.App.Core.Models.Validation; +using Microsoft.Extensions.Logging; + +namespace Altinn.App.Core.Features.Validation.Default; + +/// +/// Validates form data against the XSD schema for the data model, if it exists +/// +public class XsdValidator : IValidator +{ + private readonly ILogger _logger; + private readonly IAppResources _appResourceService; + private readonly IAppMetadata _appMetadata; + private readonly ModelSerializationService _modelSerializationService; + + /// + /// Constructor for the expression validator + /// + public XsdValidator( + ILogger logger, + IAppResources appResourceService, + IAppMetadata appMetadata, + ModelSerializationService modelSerializationService + ) + { + _logger = logger; + _appResourceService = appResourceService; + _appMetadata = appMetadata; + _modelSerializationService = modelSerializationService; + } + + /// + /// We implement instead + /// + public string TaskId => "*"; + + /// + /// Only run for tasks that specifies a layout set + /// + public bool ShouldRunForTask(string taskId) => + _appMetadata + .GetApplicationMetadata() + .Result.DataTypes.Exists(dt => dt.TaskId == taskId && dt.AppLogic?.ClassRef is not null); + + /// + public string ValidationSource => "Xsd"; + + /// + public bool NoIncrementalValidation => true; + + /// + /// This is not used for incremental validation + /// + public Task HasRelevantChanges( + IInstanceDataAccessor dataAccessor, + string taskId, + DataElementChanges changes + ) => Task.FromResult(true); + + /// + public async Task> Validate( + IInstanceDataAccessor dataAccessor, + string taskId, + string? language + ) + { + var validationIssues = new List(); + foreach (var (dataType, dataElement) in dataAccessor.GetDataElementsForTask(taskId)) + { + var schema = _appResourceService.GetXsdSchema(dataType.Id); + if (schema is null) + { + _logger.LogWarning( + "No XSD schema found for data type {DataTypeId}, skipping XSD validation", + dataType.Id + ); + continue; + } + var formData = await dataAccessor.GetFormData(dataElement); + ObjectUtils.RemoveAltinnRowId(formData); + + var serializedFormData = _modelSerializationService.SerializeToXml(formData); + var parsedSchema = new XmlSchemaSet(); + using (var xsdReader = XmlReader.Create(new StringReader(schema))) + { + parsedSchema.Add(null, xsdReader); + } + var settings = new XmlReaderSettings { ValidationType = ValidationType.Schema, Schemas = parsedSchema }; + settings.ValidationEventHandler += (sender, e) => + { + validationIssues.Add( + new ValidationIssue() + { + Code = "Xsd", + CustomTextKey = "backend.xsd_validation", + DataElementId = dataElement.Id, + Severity = ValidationIssueSeverity.Error, + CustomTextParameters = new Dictionary() + { + { "schema", dataType.Id }, + { "message", e.Message }, + }, + } + ); + }; + + using var xmlStream = new MemoryStream(serializedFormData.ToArray(), writable: false); + using var reader = XmlReader.Create(xmlStream, settings); + while (reader.Read()) { } + } + + return validationIssues; + } +} diff --git a/src/Altinn.App.Core/Internal/Texts/TranslationService.cs b/src/Altinn.App.Core/Internal/Texts/TranslationService.cs index 7c9c05cd47..ffe642fc6b 100644 --- a/src/Altinn.App.Core/Internal/Texts/TranslationService.cs +++ b/src/Altinn.App.Core/Internal/Texts/TranslationService.cs @@ -318,6 +318,17 @@ await state.GetModelData(binding, context?.DataElementIdentifier, context?.RowIn _ => "Dokumentet er en forhåndsvisning", }, }; + case "backend.xsd_validation": + return new TextResourceElement() + { + Id = "backend.xsd_validation", + Value = language switch + { + LanguageConst.Nb => "Ett eller flere felter bryter reglene satt av XSD", + LanguageConst.Nn => "Eitt eller fleire felt bryt reglane sette av XSD", + _ => "One or more field is violating a rule in the XSD", + }, + }; } return null; From 69ace7b4a06472af84537eac82aea35e9c4a9890 Mon Sep 17 00:00:00 2001 From: MagnusNordboe Date: Mon, 23 Feb 2026 09:51:07 +0100 Subject: [PATCH 2/3] mer korrekt tekst --- src/Altinn.App.Core/Internal/Texts/TranslationService.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Altinn.App.Core/Internal/Texts/TranslationService.cs b/src/Altinn.App.Core/Internal/Texts/TranslationService.cs index ffe642fc6b..6bd82be86e 100644 --- a/src/Altinn.App.Core/Internal/Texts/TranslationService.cs +++ b/src/Altinn.App.Core/Internal/Texts/TranslationService.cs @@ -324,9 +324,9 @@ await state.GetModelData(binding, context?.DataElementIdentifier, context?.RowIn Id = "backend.xsd_validation", Value = language switch { - LanguageConst.Nb => "Ett eller flere felter bryter reglene satt av XSD", - LanguageConst.Nn => "Eitt eller fleire felt bryt reglane sette av XSD", - _ => "One or more field is violating a rule in the XSD", + LanguageConst.Nb => "Et felt bryter reglene satt av XSD", + LanguageConst.Nn => "Eit felt bryt reglane sette av XSD", + _ => "A field is in violation of the rules set by the XSD schema", }, }; } From a25bd520f4eea8e0b88c11c1e6aea18358a99747 Mon Sep 17 00:00:00 2001 From: MagnusNordboe Date: Fri, 27 Feb 2026 14:28:15 +0100 Subject: [PATCH 3/3] tilbakemeldinger --- .../Altinn.App.Api.Experimental.props | 27 +++++++++++++++++++ .../Validation/Default/XsdValidator.cs | 2 +- .../Internal/Texts/TranslationService.cs | 14 +++++++--- 3 files changed, 39 insertions(+), 4 deletions(-) create mode 100644 src/Altinn.App.Api/packaging/Altinn.App.Api.Experimental.props diff --git a/src/Altinn.App.Api/packaging/Altinn.App.Api.Experimental.props b/src/Altinn.App.Api/packaging/Altinn.App.Api.Experimental.props new file mode 100644 index 0000000000..2f8e782be2 --- /dev/null +++ b/src/Altinn.App.Api/packaging/Altinn.App.Api.Experimental.props @@ -0,0 +1,27 @@ + + + + + true + + + + + + + + + + + diff --git a/src/Altinn.App.Core/Features/Validation/Default/XsdValidator.cs b/src/Altinn.App.Core/Features/Validation/Default/XsdValidator.cs index ece117e1e7..66c346ea9e 100644 --- a/src/Altinn.App.Core/Features/Validation/Default/XsdValidator.cs +++ b/src/Altinn.App.Core/Features/Validation/Default/XsdValidator.cs @@ -76,7 +76,7 @@ public async Task> Validate( var schema = _appResourceService.GetXsdSchema(dataType.Id); if (schema is null) { - _logger.LogWarning( + _logger.LogInformation( "No XSD schema found for data type {DataTypeId}, skipping XSD validation", dataType.Id ); diff --git a/src/Altinn.App.Core/Internal/Texts/TranslationService.cs b/src/Altinn.App.Core/Internal/Texts/TranslationService.cs index 6bd82be86e..3361d3508c 100644 --- a/src/Altinn.App.Core/Internal/Texts/TranslationService.cs +++ b/src/Altinn.App.Core/Internal/Texts/TranslationService.cs @@ -324,10 +324,18 @@ await state.GetModelData(binding, context?.DataElementIdentifier, context?.RowIn Id = "backend.xsd_validation", Value = language switch { - LanguageConst.Nb => "Et felt bryter reglene satt av XSD", - LanguageConst.Nn => "Eit felt bryt reglane sette av XSD", - _ => "A field is in violation of the rules set by the XSD schema", + LanguageConst.Nb => "Et felt bryter reglene satt av XSD. Melding: {0]", + LanguageConst.Nn => "Eit felt bryt reglane sette av XSD. Melding: {0}", + _ => "A field is in violation of the rules set by the XSD schema. Message: {0}", }, + Variables = [ + new TextResourceVariable() + { + DataSource = "customTextParameters", + Key = "message", + DefaultValue = "" + } + ] }; }