Skip to content
Closed
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
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
<PackageVersion Include="System.Linq.Async" Version="6.0.3" />
<PackageVersion Include="Testcontainers" Version="4.4.0" />
<PackageVersion Include="Verify.AspNetCore" Version="4.0.0" />
<PackageVersion Include="Verify.Moq" Version="2.2.0" />
<PackageVersion Include="Verify.Xunit" Version="30.4.0" />
<PackageVersion Include="Verify.Http" Version="6.6.0" />
<PackageVersion Include="WireMock.Net" Version="1.8.17" />
Expand Down
16 changes: 11 additions & 5 deletions src/Altinn.App.Core/Features/IFormDataValidator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,19 @@ public interface IFormDataValidator
/// </summary>
string ValidationSource => $"{this.GetType().FullName}-{DataType}";

/// <summary>
/// If you override this to return true, the validator will only run on process/next, and not continuously.
/// <see cref="HasRelevantChanges"/> will never get called
/// <see cref="IValidator.NoIncrementalValidation"/>
/// </summary>
/// <inheritdoc cref="IValidator.NoIncrementalValidation"/>
/// <remarks>
/// <see cref="IFormDataValidator"/> will run on incremental changes using <see cref="HasRelevantChanges"/>.
/// </remarks>
bool NoIncrementalValidation => false;

/// <inheritdoc cref="IValidator.ShouldRunAfterRemovingHiddenData"/>
/// <remarks>
/// Defaults to full data. When <c>true</c>, the pipeline provides a cleaned data accessor to
/// both <see cref="ValidateFormData"/> and <see cref="HasRelevantChanges"/> for consistent visibility.
/// </remarks>
bool ShouldRunAfterRemovingHiddenData => false;

/// <summary>
/// The actual validation function
/// </summary>
Expand Down
13 changes: 13 additions & 0 deletions src/Altinn.App.Core/Features/IInstanceDataAccessor.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using Altinn.App.Core.Helpers;
using Altinn.App.Core.Models;
using Altinn.Platform.Storage.Interface.Models;

Expand Down Expand Up @@ -32,6 +33,18 @@ public interface IInstanceDataAccessor
/// <exception cref="InvalidOperationException">when identifier does not exist in instance.Data with an applogic data type</exception>
Task<IFormDataWrapper> GetFormDataWrapper(DataElementIdentifier dataElementIdentifier);

/// <summary>
/// Get a <see cref="IInstanceDataAccessor"/> that provides access to the cleaned data (where all fields marked as "hidden" are removed).
/// </summary>
/// <param name="rowRemovalOption">The strategy for "hiddenRow" on group components</param>
IInstanceDataAccessor GetCleanAccessor(RowRemovalOption rowRemovalOption = RowRemovalOption.SetToNull);

Comment thread
ivarne marked this conversation as resolved.
/// <summary>
/// Get a <see cref="IInstanceDataAccessor"/> that provides access to the
/// storage persisted before any in-memory changes in this request.
/// </summary>
IInstanceDataAccessor GetPreviousDataAccessor();

/// <summary>
/// Gets the raw binary data from a DataElement.
/// </summary>
Expand Down
9 changes: 9 additions & 0 deletions src/Altinn.App.Core/Features/IValidator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,15 @@ public interface IValidator
/// </summary>
bool NoIncrementalValidation => false;

/// <summary>
/// Indicates whether this validator should run against a cleaned view of the data where fields marked as hidden are removed.
/// </summary>
/// <remarks>
/// Defaults to <c>false</c>. When <c>true</c>, the validation pipeline will supply a cleaned accessor for both
/// <see cref="Validate"/> and <see cref="HasRelevantChanges"/>, ensuring consistent visibility semantics.
/// </remarks>
bool ShouldRunAfterRemovingHiddenData => false;

/// <summary>
/// Run this validator and return all the issues this validator is aware of.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ private static void InitValidation(InitContext context)
ActivitySource
.StartActivity($"{Prefix}.RunValidator")
?.SetTag(InternalLabels.ValidatorType, validator.GetType().Name)
.SetTag(InternalLabels.ValidatorSource, validator.ValidationSource);
.SetTag(InternalLabels.ValidatorSource, validator.ValidationSource)
.SetTag(InternalLabels.ValidatorRemoveHiddenData, validator.ShouldRunAfterRemovingHiddenData);

internal static class Validation
{
Expand Down
1 change: 1 addition & 0 deletions src/Altinn.App.Core/Features/Telemetry/Telemetry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,7 @@ internal static class InternalLabels
internal const string AuthorizerTaskId = "authorization.authorizer.task.id";
internal const string ValidatorType = "validator.type";
internal const string ValidatorSource = "validator.source";
internal const string ValidatorRemoveHiddenData = "validator.remove_hidden_data";
internal const string ValidatorHasRelevantChanges = "validator.has_relevant_changes";
internal const string ValidatorChangedElementsIds = "validator.changed_elements_ids";
internal const string ValidatorIssueCount = "validation.issue_count";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ public string ValidationSource
{
var type = _instanceValidator?.GetType() ?? GetType();
Debug.Assert(type.FullName is not null, "FullName does not return null on class/struct types");
return type.FullName;
return type.FullName + "_FormData";
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public string ValidationSource
{
var type = _instanceValidator.GetType();
Debug.Assert(type.FullName is not null, "FullName does not return null on class/struct types");
return type.FullName;
return type.FullName + "_Task";
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ protected GenericFormDataValidator(string dataType)
/// <inheritdoc/>
public virtual bool NoIncrementalValidation => false;

/// <inheritdoc />
public virtual bool ShouldRunAfterRemovingHiddenData => false;

// ReSharper disable once StaticMemberInGenericType
private static readonly AsyncLocal<List<ValidationIssue>> _validationIssues = new();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ IDataElementAccessChecker dataElementAccessChecker
/// <inheritdoc />
public bool NoIncrementalValidation => _formDataValidator.NoIncrementalValidation;

/// <inheritdoc />
public bool ShouldRunAfterRemovingHiddenData => _formDataValidator.ShouldRunAfterRemovingHiddenData;

/// <summary>
/// Run all legacy <see cref="IDataElementValidator"/> instances for the given <see cref="DataType"/>.
/// </summary>
Expand Down
148 changes: 148 additions & 0 deletions src/Altinn.App.Core/Internal/Data/CleanInstanceDataAccessor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
using Altinn.App.Core.Configuration;
using Altinn.App.Core.Features;
using Altinn.App.Core.Helpers;
using Altinn.App.Core.Internal.App;
using Altinn.App.Core.Internal.Expressions;
using Altinn.App.Core.Internal.Texts;
using Altinn.App.Core.Models;
using Altinn.App.Core.Models.Layout;
using Altinn.Platform.Storage.Interface.Models;

namespace Altinn.App.Core.Internal.Data;

internal class CleanInstanceDataAccessor : IInstanceDataAccessor
{
private readonly IInstanceDataAccessor _dataAccessor;
private readonly string? _taskId;
private readonly IAppResources _appResources;
private readonly FrontEndSettings _frontEndSettings;
private readonly RowRemovalOption _rowRemovalOption;
private readonly string? _language;
private readonly ITranslationService _translationService;
private readonly Telemetry? _telemetry;

public CleanInstanceDataAccessor(
IInstanceDataAccessor dataAccessor,
string? taskId,
IAppResources appResources,
ITranslationService translationService,
FrontEndSettings frontEndSettings,
RowRemovalOption rowRemovalOption,
string? language,
Telemetry? telemetry
)
{
_dataAccessor = dataAccessor;
_taskId = taskId;
_appResources = appResources;
_frontEndSettings = frontEndSettings;
_rowRemovalOption = rowRemovalOption;
_language = language;
_telemetry = telemetry;
_translationService = translationService;

LayoutModel? layouts = taskId is not null ? appResources.GetLayoutModelForTask(taskId) : null;
if (layouts is null)
{
_hiddenFieldsTask = new(() => Task.FromResult(new List<DataReference>()));
}
else
{
var state = new LayoutEvaluatorState(
dataAccessor,
layouts,
translationService,
frontEndSettings,
gatewayAction: null,
language
);
_hiddenFieldsTask = new(() =>
{
using var activity = telemetry?.StartRemoveHiddenDataForValidation();
return LayoutEvaluator.GetHiddenFieldsForRemoval(state);
});
}
}

private readonly DataElementCache<IFormDataWrapper> _cleanCache = new();

private readonly Lazy<Task<List<DataReference>>> _hiddenFieldsTask;

public Instance Instance => _dataAccessor.Instance;

public IReadOnlyCollection<DataType> DataTypes => _dataAccessor.DataTypes;

public async Task<object> GetFormData(DataElementIdentifier dataElementIdentifier)
{
return (await GetFormDataWrapper(dataElementIdentifier)).BackingData<object>();
}

public async Task<IFormDataWrapper> GetFormDataWrapper(DataElementIdentifier dataElementIdentifier)
{
var dataWrapper = await _cleanCache.GetOrCreate(
dataElementIdentifier,
async () =>
{
var data = await _dataAccessor.GetFormDataWrapper(dataElementIdentifier).ConfigureAwait(false);
var hiddenFields = await _hiddenFieldsTask.Value.ConfigureAwait(false);
return CleanModel(data.Copy(), dataElementIdentifier, hiddenFields, _rowRemovalOption);
}
);

return dataWrapper.Copy();
}

private static IFormDataWrapper CleanModel(
IFormDataWrapper data,
DataElementIdentifier dataElementIdentifier,
List<DataReference> hiddenFields,
RowRemovalOption rowRemovalOption
)
{
foreach (var dataReference in hiddenFields)
{
if (dataReference.DataElementIdentifier != dataElementIdentifier)
{
continue;
}

// Note that the paths for lists is in reverse order from GetHiddenFieldsForRemoval, so we can remove them here in order
data.RemoveField(dataReference.Field, rowRemovalOption);
}

return data;
}

public IInstanceDataAccessor GetCleanAccessor(RowRemovalOption rowRemovalOption = RowRemovalOption.SetToNull)
{
if (rowRemovalOption == _rowRemovalOption)
{
return this;
}
return new CleanInstanceDataAccessor(
_dataAccessor,
_taskId,
_appResources,
_translationService,
_frontEndSettings,
rowRemovalOption,
_language,
_telemetry
);
}

public IInstanceDataAccessor GetPreviousDataAccessor()
{
return _dataAccessor.GetPreviousDataAccessor();
}

public async Task<ReadOnlyMemory<byte>> GetBinaryData(DataElementIdentifier dataElementIdentifier)
{
return await _dataAccessor.GetBinaryData(dataElementIdentifier);
}

public DataElement GetDataElement(DataElementIdentifier dataElementIdentifier)
{
return _dataAccessor.GetDataElement(dataElementIdentifier);
}
}
2 changes: 1 addition & 1 deletion src/Altinn.App.Core/Internal/Data/DataElementCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public async Task<T> GetOrCreate(DataElementIdentifier key, Func<Task<T>> valueF
_cache.Add(key.Guid, lazyTask);
}
}
return await lazyTask.Value;
return await lazyTask.Value.ConfigureAwait(false);
}

public void Set(DataElementIdentifier key, T data)
Expand Down
59 changes: 48 additions & 11 deletions src/Altinn.App.Core/Internal/Data/InstanceDataUnitOfWork.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,11 @@ internal sealed class InstanceDataUnitOfWork : IInstanceDataMutator
private readonly ApplicationMetadata _appMetadata;
private readonly ModelSerializationService _modelSerializationService;

// private readonly IAppResources _appResources;
// private readonly IOptions<FrontEndSettings> _frontEndSettings;
// private readonly string? _taskId;
// private readonly string? _language;
private readonly IAppResources _appResources;
private readonly IOptions<FrontEndSettings> _frontEndSettings;
private readonly string? _taskId;
private readonly string? _language;
private readonly ITranslationService _translationService;
private readonly Telemetry? _telemetry;

// Cache for the most up-to-date form data (can be mutated or replaced with SetFormData(dataElementId, data))
Expand All @@ -51,8 +52,6 @@ internal sealed class InstanceDataUnitOfWork : IInstanceDataMutator
// Form data not yet saved to storage (thus no dataElementId)
private readonly ConcurrentBag<DataElementChange> _changesForCreation = [];

// private readonly ITranslationService _translationService;

public InstanceDataUnitOfWork(
Instance instance,
IDataClient dataClient,
Expand All @@ -78,12 +77,12 @@ public InstanceDataUnitOfWork(
DataTypes = appMetadata.DataTypes;
_dataClient = dataClient;
_appMetadata = appMetadata;
// _translationService = translationService;
_translationService = translationService;
_modelSerializationService = modelSerializationService;
// _taskId = taskId;
// _language = language;
// _frontEndSettings = frontEndSettings;
// _appResources = appResources;
_taskId = taskId;
_language = language;
_frontEndSettings = frontEndSettings;
_appResources = appResources;
_instanceClient = instanceClient;
_telemetry = telemetry;
}
Expand Down Expand Up @@ -121,6 +120,44 @@ public async Task<IFormDataWrapper> GetFormDataWrapper(DataElementIdentifier dat
);
}

/// <inheritdoc />
public IInstanceDataAccessor GetCleanAccessor(RowRemovalOption rowRemovalOption = RowRemovalOption.SetToNull)
{
return new CleanInstanceDataAccessor(
this,
_taskId,
_appResources,
_translationService,
_frontEndSettings.Value,
rowRemovalOption,
_language,
_telemetry
);
}

// Non thread safe cache, because the previous data is always the same.
private PreviousDataAccessor? _previousDataAccessorCache;

public IInstanceDataAccessor GetPreviousDataAccessor()
{
if (_previousDataAccessorCache is not null)
{
return _previousDataAccessorCache;
}

_previousDataAccessorCache = new PreviousDataAccessor(
this,
_taskId,
_appResources,
_translationService,
_modelSerializationService,
_frontEndSettings.Value,
_language,
_telemetry
);
return _previousDataAccessorCache;
}

/// <inheritdoc />
public async Task<ReadOnlyMemory<byte>> GetBinaryData(DataElementIdentifier dataElementIdentifier)
{
Expand Down
Loading
Loading