-
Notifications
You must be signed in to change notification settings - Fork 11
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
9 changed files
with
636 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
using Altinn.App.Core.Models.Validation; | ||
using Altinn.Platform.Storage.Interface.Models; | ||
using Microsoft.Extensions.DependencyInjection; | ||
|
||
namespace Altinn.App.Core.Features; | ||
|
||
public interface IFormDataValidator | ||
{ | ||
/// <summary> | ||
/// The data type this validator is for. Typically either hard coded by implementation or | ||
/// or set by constructor using a <see cref="ServiceKeyAttribute" /> and a keyed service. | ||
/// </summary> | ||
string DataType { get; } | ||
|
||
/// <summary> | ||
/// Used for partial validation to ensure that the validator only runs | ||
/// </summary> | ||
bool ShouldRun(List<string>? changedFields = null); | ||
|
||
/// <summary> | ||
/// Returns the group id of the validator. This is used to run partial validations on the backend. | ||
/// </summary> | ||
/// <remarks> | ||
/// The default implementation only works if a validator class is registered once. | ||
/// If your validator can be registered multiple times, you need to override this method. | ||
/// </remarks> | ||
public string Code => this.GetType().FullName ?? string.Empty; | ||
|
||
/// <summary> | ||
/// | ||
/// </summary> | ||
/// <param name="instance"></param> | ||
/// <param name="dataElement"></param> | ||
/// <param name="data"></param> | ||
/// <param name="changedFields"></param> | ||
/// <returns>List of validation issues</returns> | ||
Task<List<ValidationIssue>> ValidateFormData(Instance instance, DataElement dataElement, object data, List<string>? changedFields = null); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
using Altinn.App.Core.Models.Validation; | ||
using Altinn.Platform.Storage.Interface.Models; | ||
using Microsoft.Extensions.DependencyInjection; | ||
|
||
namespace Altinn.App.Core.Features; | ||
|
||
/// <summary> | ||
/// Interface for handling validation of tasks. | ||
/// </summary> | ||
public interface ITaskValidator | ||
{ | ||
/// <summary> | ||
/// The task id this validator is for. Typically either hard coded by implementation or | ||
/// or set by constructor using a <see cref="ServiceKeyAttribute" /> and a keyed service. | ||
/// </summary> | ||
/// <example> | ||
/// <code> | ||
/// string TaskId { get; init; } | ||
/// // constructor | ||
/// public MyTaskValidator([ServiceKey] string taskId) | ||
/// { | ||
/// TaskId = taskId; | ||
/// } | ||
/// </code> | ||
/// </example> | ||
string TaskId { get; } | ||
|
||
/// <summary> | ||
/// Unique code for the validator. Used to run partial validations on the backend. | ||
/// </summary> | ||
public string Code => this.GetType().FullName ?? string.Empty; | ||
|
||
/// <summary> | ||
/// Actual validation logic for the task | ||
/// </summary> | ||
/// <param name="instance">The instance to validate</param> | ||
/// <returns>List of validation issues to add to this task validation</returns> | ||
Task<List<ValidationIssue>> ValidateTask(Instance instance); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
using System.Collections.Specialized; | ||
using Altinn.App.Core.Models.Validation; | ||
using Altinn.Platform.Storage.Interface.Models; | ||
|
||
namespace Altinn.App.Core.Features; | ||
|
||
/// <summary> | ||
/// Core interface for validation of instances. Only a single implementation of this interface should exist in the app. | ||
/// </summary> | ||
public interface IValidationService | ||
{ | ||
/// <summary> | ||
/// Validates the instance with all data elements on the current task and ensures that the instance is read for process next. | ||
/// </summary> | ||
Task<List<ValidationIssue>> ValidateTask(Instance instance); | ||
|
||
/// <summary> | ||
/// Validates a single data element. Used by frontend to continuously validate form data. | ||
/// </summary> | ||
Task<Dictionary<string,List<ValidationIssue>>> ValidateDataElement(Instance instance, DataElement dataType, object data, List<string>? changedFields = null); | ||
} |
108 changes: 108 additions & 0 deletions
108
src/Altinn.App.Core/Features/Validation/GenericFormDataValidator.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,108 @@ | ||
using System.Diagnostics; | ||
using System.Linq.Expressions; | ||
using Altinn.App.Core.Helpers; | ||
using Altinn.App.Core.Models.Validation; | ||
using Altinn.Platform.Storage.Interface.Models; | ||
|
||
namespace Altinn.App.Core.Features.Validation; | ||
|
||
/// <summary> | ||
/// Simple wrapper for validation of form data that does the type checking for you. | ||
/// </summary> | ||
/// <typeparam name="TModel">The type of the model this class will validate</typeparam> | ||
public abstract class GenericFormDataValidator<TModel> : IFormDataValidator | ||
{ | ||
/// <summary> | ||
/// Constructor to force the DataType to be set. | ||
/// </summary> | ||
/// <param name="dataType"></param> | ||
protected GenericFormDataValidator(string dataType) | ||
{ | ||
DataType = dataType; | ||
} | ||
/// <inheritdoc /> | ||
public string DataType { get; private init; } | ||
|
||
private readonly List<string> _runForPrefixes = new List<string>(); | ||
// ReSharper disable once StaticMemberInGenericType | ||
private static readonly AsyncLocal<List<ValidationIssue>> ValidationIssues = new(); | ||
|
||
/// <summary> | ||
/// Default implementation that respects the runFor prefixes. | ||
/// </summary> | ||
public bool ShouldRun(List<string>? changedFields = null) | ||
{ | ||
if (changedFields == null) | ||
{ | ||
return true; | ||
} | ||
|
||
if (_runForPrefixes.Count == 0) | ||
{ | ||
return true; | ||
} | ||
|
||
foreach (var prefix in _runForPrefixes) | ||
{ | ||
foreach (var changedField in changedFields) | ||
{ | ||
if (IsMatch(changedField, prefix)) | ||
{ | ||
return true; | ||
} | ||
} | ||
Check notice Code scanning / CodeQL Missed opportunity to use Where Note
This foreach loop
implicitly filters its target sequence Error loading related location Loading |
||
} | ||
|
||
return false; | ||
} | ||
|
||
private static bool IsMatch(string changedField, string prefix) | ||
{ | ||
return changedField.StartsWith(prefix) || prefix.StartsWith(changedField); | ||
} | ||
|
||
/// <summary> | ||
/// Easy way to configure <see cref="ShouldRun"/> to only run for fields that start with the given prefix. | ||
/// </summary> | ||
/// <param name="selector"></param> | ||
/// <typeparam name="T1"></typeparam> | ||
protected void RunFor<T1>(Expression<Func<TModel, T1>> selector) | ||
{ | ||
_runForPrefixes.Add(LinqExpressionHelpers.GetJsonPath(selector)); | ||
} | ||
|
||
protected void CreateValidationIssue<T>(Expression<Func<TModel,T>> selector, string textKey, ValidationIssueSeverity severity = ValidationIssueSeverity.Error) | ||
{ | ||
Debug.Assert(ValidationIssues.Value is not null); | ||
ValidationIssues.Value.Add( new ValidationIssue | ||
{ | ||
Field = LinqExpressionHelpers.GetJsonPath(selector), | ||
CustomTextKey = textKey, | ||
Severity = severity | ||
}); | ||
} | ||
|
||
protected void AddValidationIssue(ValidationIssue issue) | ||
{ | ||
Debug.Assert(ValidationIssues.Value is not null); | ||
ValidationIssues.Value.Add(issue); | ||
} | ||
|
||
public async Task<List<ValidationIssue>> ValidateFormData(Instance instance, DataElement dataElement, object data, List<string>? changedFields = null) | ||
{ | ||
if (data is not TModel model) | ||
{ | ||
throw new ArgumentException($"Data is not of type {typeof(TModel)}"); | ||
} | ||
|
||
ValidationIssues.Value = new List<ValidationIssue>();; | ||
await ValidateFormData(instance, dataElement, model); | ||
return ValidationIssues.Value; | ||
|
||
} | ||
|
||
/// <summary> | ||
/// Implement this method to validate the data. | ||
/// </summary> | ||
protected abstract Task ValidateFormData(Instance instance, DataElement dataElement, TModel data, List<string>? changedFields = null); | ||
} |
119 changes: 119 additions & 0 deletions
119
src/Altinn.App.Core/Features/Validation/ValidationService.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,119 @@ | ||
using Altinn.App.Core.Internal.App; | ||
using Altinn.App.Core.Internal.AppModel; | ||
using Altinn.App.Core.Internal.Data; | ||
using Altinn.App.Core.Models.Validation; | ||
using Altinn.Platform.Storage.Interface.Models; | ||
using Microsoft.Extensions.DependencyInjection; | ||
|
||
namespace Altinn.App.Core.Features.Validation; | ||
|
||
public class ValidationService : IValidationService | ||
{ | ||
private readonly IServiceProvider _serviceProvider; | ||
private readonly IDataClient _dataClient; | ||
private readonly IAppModel _appModel; | ||
private readonly IAppMetadata _appMetadata; | ||
|
||
public ValidationService(IServiceProvider serviceProvider, IDataClient dataClient, IAppModel appModel, IAppMetadata appMetadata) | ||
{ | ||
_serviceProvider = serviceProvider; | ||
_dataClient = dataClient; | ||
_appModel = appModel; | ||
_appMetadata = appMetadata; | ||
} | ||
|
||
/// <summary> | ||
/// Run all validations for a task, and return a list of issues. | ||
/// </summary> | ||
/// <param name="instance">The instance to validate</param> | ||
/// <returns>List of validation issues</returns> | ||
public async Task<List<ValidationIssue>> ValidateTask(Instance instance) | ||
{ | ||
var issues = new List<ValidationIssue>(); | ||
var taskId = instance.Process.CurrentTask.Name; | ||
|
||
// Run task validations | ||
var taskValidators = _serviceProvider.GetServices<ITaskValidator>(); | ||
taskValidators = taskValidators.Concat(_serviceProvider.GetKeyedServices<ITaskValidator>(taskId)); | ||
foreach (var taskValidator in taskValidators) | ||
{ | ||
var code = taskValidator.Code; | ||
if (taskValidator.TaskId == taskId) | ||
{ | ||
var taskIssues = await taskValidator.ValidateTask(instance); | ||
taskIssues.ForEach(i=>i.Code = code); | ||
issues.AddRange(taskIssues); | ||
} | ||
} | ||
|
||
// Run data validations | ||
var appMetadata = await _appMetadata.GetApplicationMetadata(); | ||
var dataTypes = appMetadata.DataTypes.Where(dt => dt.TaskId == taskId); | ||
foreach (var dataElement in instance.Data) | ||
{ | ||
// ReSharper disable once PossibleMultipleEnumeration | ||
var dataType = dataTypes.FirstOrDefault(dt => dt.Id == dataElement.DataType); | ||
if(dataType is null) continue; // Ignore data elements that are not part of the current task | ||
var dataValidators = _serviceProvider.GetServices<IFormDataValidator>(); | ||
dataValidators = dataValidators.Concat(_serviceProvider.GetKeyedServices<IFormDataValidator>(dataElement.DataType)); | ||
Type modelType = _appModel.GetModelType(dataType.AppLogic.ClassRef); | ||
Guid instanceGuid = Guid.Parse(instance.Id.Split("/")[1]); | ||
string app = instance.AppId.Split("/")[1]; | ||
int instanceOwnerPartyId = int.Parse(instance.InstanceOwner.PartyId); | ||
var data = await _dataClient.GetFormData(instanceGuid, modelType, instance.Org, app, instanceOwnerPartyId, Guid.Parse(dataElement.Id)); // TODO: Add method that accepts instance and dataElement | ||
foreach (var dataValidator in dataValidators) | ||
{ | ||
if (dataValidator.DataType == dataElement.DataType) | ||
{ | ||
// TODO: ensure validators run in parallel | ||
var dataIssues = await dataValidator.ValidateFormData(instance, dataElement, data); | ||
dataIssues.ForEach(i=>i.Code = dataValidator.Code); | ||
issues.AddRange(dataIssues); | ||
} | ||
} | ||
Check notice Code scanning / CodeQL Missed opportunity to use Where Note
This foreach loop
implicitly filters its target sequence Error loading related location Loading |
||
} | ||
|
||
return issues; | ||
} | ||
|
||
/// <inheritdoc/> | ||
public async Task<Dictionary<string,List<ValidationIssue>>> ValidateDataElement(Instance instance, DataElement dataElement, object data, List<string>? changedFields = null) | ||
{ | ||
var issues = new Dictionary<string,List<ValidationIssue>>(); | ||
|
||
// Locate the relevant data validator services from normal and keyed services | ||
var dataValidators = _serviceProvider.GetServices<IFormDataValidator>(); | ||
if (dataElement.DataType is not null) | ||
{ | ||
dataValidators = | ||
dataValidators.Concat(_serviceProvider.GetKeyedServices<IFormDataValidator>(dataElement.DataType)); | ||
} | ||
|
||
// Start the validators for this data type if they should run given the changed fields | ||
List<Task<List<ValidationIssue>>> validationTasks = new (); | ||
List<string> validationCodes = new (); | ||
foreach (var dataValidator in dataValidators) | ||
{ | ||
if (dataValidator.DataType == dataElement.DataType) | ||
{ | ||
if (dataValidator.ShouldRun(changedFields)) | ||
{ | ||
validationTasks.Add(dataValidator.ValidateFormData(instance, dataElement, data)); | ||
validationCodes.Add(dataValidator.Code); | ||
} | ||
} | ||
Check notice Code scanning / CodeQL Nested 'if' statements can be combined Note
These 'if' statements can be combined.
|
||
} | ||
Check notice Code scanning / CodeQL Missed opportunity to use Where Note
This foreach loop
implicitly filters its target sequence Error loading related location Loading |
||
|
||
// Ensure that all validators are run in parallel | ||
List<ValidationIssue>[] validationIssues = await Task.WhenAll(validationTasks); | ||
|
||
// Add the validation issues to the dictionary with the correct code | ||
for (var i = 0; i < validationIssues.Length; i++) | ||
{ | ||
validationIssues[i].ForEach(vi => vi.Code = validationCodes[i]); | ||
issues.Add(validationCodes[i], validationIssues[i]); | ||
} | ||
|
||
return issues; | ||
} | ||
} |
Oops, something went wrong.