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
3 changes: 2 additions & 1 deletion src/Altinn.App.Api/Controllers/ActionsController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using Altinn.App.Core.Features;
using Altinn.App.Core.Features.Action;
using Altinn.App.Core.Features.Auth;
using Altinn.App.Core.Helpers.DataModel;
using Altinn.App.Core.Internal.Data;
using Altinn.App.Core.Internal.Instances;
using Altinn.App.Core.Internal.Validation;
Expand Down Expand Up @@ -203,7 +204,7 @@ await Task.WhenAll(
{
// If the data mutator missed a that was returned with the deprecated UpdatedDataModels
// we still need to return it to the frontend, but we assume it was already saved to storage
dataMutator.SetFormData(new DataElementIdentifier(elementId), data);
dataMutator.SetFormData(new DataElementIdentifier(elementId), FormDataWrapperFactory.Create(data));
}
}
#pragma warning restore CS0618 // Type or member is obsolete
Expand Down
7 changes: 4 additions & 3 deletions src/Altinn.App.Api/Controllers/DataController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
using Altinn.App.Core.Features.FileAnalysis;
using Altinn.App.Core.Features.FileAnalyzis;
using Altinn.App.Core.Helpers;
using Altinn.App.Core.Helpers.DataModel;
using Altinn.App.Core.Helpers.Serialization;
using Altinn.App.Core.Internal.App;
using Altinn.App.Core.Internal.AppModel;
Expand Down Expand Up @@ -1118,16 +1119,16 @@ private async Task<ActionResult> PutFormData(
// Get the previous service model for dataProcessing to work
var oldServiceModel = await dataMutator.GetFormData(dataElement);
// Set the new service model so that dataAccessors see the new state
dataMutator.SetFormData(dataElement, serviceModel);
dataMutator.SetFormData(dataElement, FormDataWrapperFactory.Create(serviceModel));

var requestedChange = new FormDataChange()
{
Type = ChangeType.Updated,
DataElement = dataElement,
ContentType = dataElement.ContentType,
DataType = dataType,
PreviousFormData = oldServiceModel,
CurrentFormData = serviceModel,
PreviousFormDataWrapper = FormDataWrapperFactory.Create(oldServiceModel),
CurrentFormDataWrapper = FormDataWrapperFactory.Create(serviceModel),
PreviousBinaryData = await dataMutator.GetBinaryData(dataElement),
CurrentBinaryData = null, // We don't serialize to xml before running data processors
};
Expand Down
11 changes: 6 additions & 5 deletions src/Altinn.App.Api/Helpers/Patch/InternalPatchService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using Altinn.App.Api.Extensions;
using Altinn.App.Api.Models;
using Altinn.App.Core.Features;
using Altinn.App.Core.Helpers.DataModel;
using Altinn.App.Core.Internal.Data;
using Altinn.App.Core.Internal.Validation;
using Altinn.App.Core.Models;
Expand Down Expand Up @@ -117,7 +118,7 @@ public async Task<ServiceResult<DataPatchResult, ProblemDetails>> ApplyPatches(

var newModel = newModelResult.Ok;
// Reset dataAccessor to provide the patched model.
dataAccessor.SetFormData(dataElement, newModel);
dataAccessor.SetFormData(dataElementIdentifier, FormDataWrapperFactory.Create(newModel));

changesAfterPatch.Add(
new FormDataChange
Expand All @@ -126,8 +127,8 @@ public async Task<ServiceResult<DataPatchResult, ProblemDetails>> ApplyPatches(
DataElement = dataElement,
ContentType = dataElement.ContentType,
DataType = dataAccessor.GetDataType(dataElementIdentifier),
PreviousFormData = oldModel,
CurrentFormData = newModel,
PreviousFormDataWrapper = FormDataWrapperFactory.Create(oldModel),
CurrentFormDataWrapper = FormDataWrapperFactory.Create(newModel),
PreviousBinaryData = await dataAccessor.GetBinaryData(dataElementIdentifier),
CurrentBinaryData = null, // Set this after DataProcessors have run
}
Expand Down Expand Up @@ -191,8 +192,8 @@ await RunDataProcessors(
DataElement = dataElement,
ContentType = dataElement.ContentType,
DataType = dataAccessor.GetDataType(dataElement),
PreviousFormData = await dataAccessor.GetFormData(dataElement),
CurrentFormData = await dataAccessor.GetFormData(dataElement),
PreviousFormDataWrapper = await dataAccessor.GetFormDataWrapper(dataElement),
CurrentFormDataWrapper = await dataAccessor.GetFormDataWrapper(dataElement),
PreviousBinaryData = await dataAccessor.GetBinaryData(dataElement),
CurrentBinaryData = await dataAccessor.GetBinaryData(dataElement),
}
Expand Down
130 changes: 108 additions & 22 deletions src/Altinn.App.Core/Features/IInstanceDataAccessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,25 @@ public interface IInstanceDataAccessor
/// </summary>
Instance Instance { get; }

/// <summary>
/// Get the data types from application metadata.
/// </summary>
IReadOnlyCollection<DataType> DataTypes { get; }

/// <summary>
/// Get the actual data represented in the data element.
/// </summary>
/// <returns>The deserialized data model for this data element</returns>
/// <exception cref="InvalidOperationException">when identifier does not exist in instance.Data with an applogic data type</exception>
Task<object> GetFormData(DataElementIdentifier dataElementIdentifier);

/// <summary>
/// Get the actual data represented in the data element wrapped in an <see cref="IFormDataWrapper"/>.
/// </summary>
/// <returns>The deserialized data model for this data element</returns>
/// <exception cref="InvalidOperationException">when identifier does not exist in instance.Data with an applogic data type</exception>
Task<IFormDataWrapper> GetFormDataWrapper(DataElementIdentifier dataElementIdentifier);

/// <summary>
/// Gets the raw binary data from a DataElement.
/// </summary>
Expand All @@ -32,20 +44,27 @@ public interface IInstanceDataAccessor
/// </summary>
/// <exception cref="InvalidOperationException">If the data element is not found on the instance</exception>
DataElement GetDataElement(DataElementIdentifier dataElementIdentifier);

/// <summary>
/// Get the data type from application with the given type.
/// </summary>
/// <param name="dataTypeId">DataType.Id (from applicationmetadata.json)</param>
/// <returns>The data type (or null if it does not exist)</returns>
DataType? GetDataType(string dataTypeId);
}

/// <summary>
/// Extension methods for IInstanceDataAccessor to simplify usage.
/// </summary>
public static class IInstanceDataAccessorExtensions
{
/// <summary>
/// Get the data type from application with the given type.
/// </summary>
/// <returns>The data type (or null if it does not exist)</returns>
public static DataType GetDataType(this IInstanceDataAccessor dataAccessor, string dataTypeId)
{
return dataAccessor.DataTypes.FirstOrDefault(dataType =>
dataTypeId.Equals(dataType.Id, StringComparison.Ordinal)
)
?? throw new InvalidOperationException(
$"Data type {dataTypeId} not found in applicationmetadata.json (found: {string.Join(", ", dataAccessor.DataTypes.Select(d => d.Id))})"
);
}

/// <summary>
/// Get the dataType of a data element.
/// </summary>
Expand All @@ -55,16 +74,34 @@ public static DataType GetDataType(
DataElementIdentifier dataElementIdentifier
)
{
var dataElement = dataAccessor.GetDataElement(dataElementIdentifier);
var dataType = dataAccessor.GetDataType(dataElement.DataType);
if (dataType is null)
var dataType = dataElementIdentifier.DataTypeId ?? dataAccessor.GetDataElement(dataElementIdentifier).DataType;
return dataAccessor.GetDataType(dataType);
}

/// <summary>
/// Get the data type from a C# class reference.
///
/// Note that this throws an error if multiple data types have a ClassRef that matches the given type.
/// </summary>
/// <exception cref="InvalidOperationException">If multiple dataType have a ClassRef that matches the given type</exception>
public static DataType GetDataType<T>(this IInstanceDataAccessor accessor)
{
var dataTypes = accessor.DataTypes.Where(d => d.AppLogic?.ClassRef == typeof(T).FullName).ToArray();
if (dataTypes.Length == 1)
{
return dataTypes[0];
}

if (dataTypes.Length == 0)
{
throw new InvalidOperationException(
$"Data type {dataElement.DataType} not found in applicationmetadata.json"
$"Data type for {typeof(T).FullName} not found in applicationmetadata.json"
);
}

return dataType;
throw new InvalidOperationException(
$"Multiple data types found that references {typeof(T).FullName} found for multiple in applicationmetadata.json ({string.Join(", ", dataTypes.Select(d => d.Id))}). This means you can't access data just based on type parameter<T>."
);
}

/// <summary>
Expand All @@ -77,12 +114,66 @@ DataElementIdentifier dataElementIdentifier
)
where T : class
{
object data = await accessor.GetFormData(dataElementIdentifier);
if (data is T t)
IFormDataWrapper data = await accessor.GetFormDataWrapper(dataElementIdentifier);
return data.BackingData<T>();
}

/// <summary>
/// Extension method to get formdata from a type parameter that must match dataType.AppLogic.ClassRef.
/// </summary>
/// <remarks>
/// This method only supports data types with MaxCount = 1.
/// </remarks>
public static async Task<T?> GetFormData<T>(this IInstanceDataAccessor accessor)
where T : class
{
var dataType = accessor.GetDataType<T>();
return await accessor.GetFormData<T>(dataType);
}

/// <summary>
/// Get form data from a specific data type. (The data type must have MaxCount = 1)
/// </summary>
public static async Task<T?> GetFormData<T>(this IInstanceDataAccessor accessor, DataType dataType)
where T : class
{
if (dataType.MaxCount != 1)
{
throw new InvalidOperationException(
$"Data type {dataType.Id} is not a single instance data type, but has MaxCount = {dataType.MaxCount}"
);
}

var dataTypeId = dataType.Id;

var dataElement = accessor.Instance.Data.FirstOrDefault(dataElement =>
dataTypeId.Equals(dataElement.DataType, StringComparison.Ordinal)
);
if (dataElement is null)
{
return t;
return null;
}
throw new InvalidOperationException($"Data element {dataElementIdentifier} is not of type {typeof(T)}");

return await accessor.GetFormData<T>(dataElement);
}

/// <summary>
/// Get an array of all the form data elements that has the given type as AppLogic.ClassRef.
/// </summary>
public static async Task<T[]> GetAllFormData<T>(this IInstanceDataAccessor accessor)
where T : class
{
var dataType = accessor.GetDataType<T>();
return await accessor.GetAllFormData<T>(dataType);
}

/// <summary>
/// Get an array of all the form data elements that has the given DataType
/// </summary>
public static async Task<T[]> GetAllFormData<T>(this IInstanceDataAccessor accessor, DataType dataType)
where T : class
{
return await Task.WhenAll(accessor.GetDataElementsForType(dataType).Select(e => accessor.GetFormData<T>(e)));
}

/// <summary>
Expand All @@ -104,8 +195,6 @@ DataType dataType
/// <summary>
/// Retrieves the data elements associated with a specific task.
/// </summary>
/// <param name="accessor">The instance data accessor.</param>
/// <param name="taskId">The identifier of the task.</param>
/// <returns>An enumerable collection of tuples containing the data type and data element associated with the specified task.</returns>
public static IEnumerable<(DataType dataType, DataElement dataElement)> GetDataElementsForTask(
this IInstanceDataAccessor accessor,
Expand Down Expand Up @@ -167,10 +256,7 @@ this IInstanceDataAccessor accessor
foreach (var dataElement in accessor.Instance.Data)
{
var dataType = accessor.GetDataType(dataElement.DataType);
if (dataType is not null)
{
yield return (dataType, dataElement);
}
yield return (dataType, dataElement);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using System.Diagnostics;
using Altinn.App.Core.Models;
using static Altinn.App.Core.Features.Telemetry.InstanceDataAccessor;

namespace Altinn.App.Core.Features;

partial class Telemetry
{
private const string ActivityName = "Telemetry.InstanceDataAccessor";

internal Activity? StartVerifyDataElementsUnchangedSincePreviousChanges()
{
var activity = ActivitySource.StartActivity($"{ActivityName}.VerifyDataElementsUnchangedSincePreviousChanges");
return activity;
}

internal Activity? StartRemoveHiddenDataForValidation()
{
return ActivitySource.StartActivity($"{Prefix}.RemoveHiddenDataForValidation");
}

internal Activity? StartSaveChanges(DataElementChanges instance)
{
var activity = ActivitySource.StartActivity($"{Prefix}.SaveChanges");
activity?.AddTag("numberOfChangedDataElements", instance.AllChanges.Count);
return activity;
}

internal static class InstanceDataAccessor
{
internal const string Prefix = "InstanceDataAccessor";
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System.Text.Json;
using Altinn.App.Core.Helpers.DataModel;
using Altinn.App.Core.Internal.App;
using Altinn.App.Core.Internal.Data;
using Altinn.App.Core.Internal.Expressions;
Expand Down Expand Up @@ -145,7 +144,7 @@ internal async Task<List<ValidationIssue>> ValidateFormData(
var context = new ComponentContext(
evaluatorState,
component: null,
rowIndices: DataModel.GetRowIndices(resolvedField.Field),
rowIndices: GetRowIndices(resolvedField.Field),
dataElementIdentifier: resolvedField.DataElementIdentifier
);
var positionalArguments = new object[] { resolvedField.Field };
Expand All @@ -166,6 +165,44 @@ await RunValidation(
return validationIssues;
}

private static int[]? GetRowIndices(string field)
{
Span<int> rowIndicesSpan = stackalloc int[200]; // Assuming max 200 indices for simplicity recursion will never go deeper than 3-4
int count = 0;
for (int index = 0; index < field.Length; index++)
{
if (field[index] == '[')
{
int startIndex = index + 1;
int endIndex = field.IndexOf(']', startIndex);
if (endIndex == -1)
{
throw new InvalidOperationException($"Unpaired [ character in field: {field}");
}
string indexString = field[startIndex..endIndex];
if (int.TryParse(indexString, out int rowIndex))
{
rowIndicesSpan[count] = rowIndex;
count++;
index = endIndex; // Move index to the end of the current bracket
}
else
{
throw new InvalidOperationException(
$"Invalid row index in field: {field} at position {startIndex}"
);
}
}
}
if (count == 0)
{
return null; // No indices found
}
int[] rowIndices = new int[count];
rowIndicesSpan[..count].CopyTo(rowIndices);
return rowIndices;
}

private async Task RunValidation(
LayoutEvaluatorState evaluatorState,
List<ValidationIssue> validationIssues,
Expand Down
Loading
Loading