Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -37,21 +37,39 @@ public ModelSerializationService(IAppModel appModel, Telemetry? telemetry = null
/// </summary>
/// <param name="data">The binary data</param>
/// <param name="dataType">The data type used to get content type and the classRef for the object to be returned</param>
/// <param name="contentType"></param>
/// <returns>The model specified in </returns>
public object DeserializeFromStorage(ReadOnlySpan<byte> data, DataType dataType)
public object DeserializeFromStorage(
ReadOnlySpan<byte> data,
DataType dataType,
string? contentType = "application/xml"
)
{
contentType ??= "application/xml";

if (dataType.AllowedContentTypes?.Contains(contentType) == false)
{
throw new InvalidOperationException(
$"Content type {contentType} is not allowed for data type {dataType.Id}"
);
}

var type = GetModelTypeForDataType(dataType);

// TODO: support sending json to storage based on dataType.ContentTypes
return DeserializeXml(data, type);
return contentType switch
{
"application/xml" => DeserializeXml(data, type),
"application/json" => DeserializeJson(data, type),
_ => throw new InvalidOperationException($"Unsupported content type {contentType}"),
};
}

/// <summary>
/// Serialize an object to binary data for storage, respecting classRef and content type in dataType
/// </summary>
/// <param name="model">The object to serialize (must match the classRef in DataType)</param>
/// <param name="dataType">The data type</param>
/// <returns>the binary data and the content type (currently only application/xml, but likely also json in the future)</returns>
/// <returns>the binary data and the content type (application/xml or application/json)</returns>
/// <exception cref="InvalidOperationException">If the classRef in dataType does not match type of the model</exception>
public (ReadOnlyMemory<byte> data, string contentType) SerializeToStorage(object model, DataType dataType)
{
Expand All @@ -63,8 +81,14 @@ public object DeserializeFromStorage(ReadOnlySpan<byte> data, DataType dataType)
);
}

//TODO: support sending json to storage based on dataType.ContentTypes
return (SerializeToXml(model), "application/xml");
var contentType = dataType.AllowedContentTypes?.FirstOrDefault() ?? "application/xml";

return contentType switch
{
"application/xml" => (SerializeToXml(model), contentType),
"application/json" => (SerializeToJson(model), contentType),
_ => throw new InvalidOperationException($"Unsupported content type {contentType}"),
};
}

/// <summary>
Expand Down
6 changes: 4 additions & 2 deletions src/Altinn.App.Core/Internal/Data/InstanceDataUnitOfWork.cs
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,8 @@ public async Task<object> GetFormData(DataElementIdentifier dataElementIdentifie

return _modelSerializationService.DeserializeFromStorage(
binaryData.Span,
this.GetDataType(dataElementIdentifier)
this.GetDataType(dataElementIdentifier),
GetDataElement(dataElementIdentifier).ContentType
);
}
);
Expand Down Expand Up @@ -359,7 +360,8 @@ out ReadOnlyMemory<byte> cachedBinary
// and deserializing twice is not a big deal
PreviousFormData = _modelSerializationService.DeserializeFromStorage(
cachedBinary.Span,
dataType
dataType,
dataElement.ContentType
),
CurrentBinaryData = currentBinary,
PreviousBinaryData = cachedBinary,
Expand Down
2 changes: 1 addition & 1 deletion test/Altinn.App.Api.Tests/Mocks/DataClientMock.cs
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ await File.ReadAllBytesAsync(dataElementPath, cancellationToken),
string dataPath = TestData.GetDataBlobPath(org, app, instanceOwnerPartyId, instanceGuid, dataId);
var dataBytes = await File.ReadAllBytesAsync(dataPath, cancellationToken);

var formData = _modelSerialization.DeserializeFromStorage(dataBytes, dataType);
var formData = _modelSerialization.DeserializeFromStorage(dataBytes, dataType, dataElement?.ContentType);

return formData ?? throw new Exception("Unable to deserialize form data");
}
Expand Down
228 changes: 228 additions & 0 deletions test/Altinn.App.Core.Tests/Models/ModelSerializationServiceTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
using Altinn.App.Core.Helpers.Serialization;
using Altinn.App.Core.Internal.AppModel;
using Altinn.Platform.Storage.Interface.Models;
using FluentAssertions;
using Moq;

namespace Altinn.App.Core.Tests.Models;

public class ModelSerializationServiceTests
{
private static readonly string _testClassRef = typeof(TestDataModel).AssemblyQualifiedName!;
private static readonly string _otherClassRef = typeof(SomeOtherType).AssemblyQualifiedName!;

private readonly ModelSerializationService _sut;

private readonly Mock<IAppModel> _appModelMock = new Mock<IAppModel>();

public ModelSerializationServiceTests()
{
_appModelMock.Setup(x => x.GetModelType(It.Is<string>(s => s == _testClassRef))).Returns(typeof(TestDataModel));
_appModelMock
.Setup(x => x.GetModelType(It.Is<string>(s => s == _otherClassRef)))
.Returns(typeof(SomeOtherType));

_sut = new ModelSerializationService(_appModelMock.Object);
}

[Theory]
[InlineData("application/json", "application/json")]
[InlineData("application/xml", "application/xml")]
[InlineData(null, "application/xml")]
public void SerializeToStorage_ReturnsExpectedContentType(string? contentType, string expectedOutputType)
{
List<string> allowedContentTypes = contentType != null ? [contentType] : [];

// Arrange
var testObject = new TestDataModel { Name = "Test", Value = 42 };

var dataType = CreateDataType(allowedContentTypes);

// Act
var (_, outputContentType) = _sut.SerializeToStorage(testObject, dataType);

// Assert
outputContentType.Should().Be(expectedOutputType);
}

[Fact]
public void SerializeToStorage_SerializesJson()
{
// Arrange
var testObject = new TestDataModel { Name = "Test", Value = 42 };

var dataType = CreateDataType(["application/json"]);

// Act
var (data, _) = _sut.SerializeToStorage(testObject, dataType);

// Assert
var json = System.Text.Encoding.UTF8.GetString(data.Span);

json.Should().Be("{\"name\":\"Test\",\"value\":42}");
}

[Fact]
public void SerializeToStorage_SerializesXml()
{
// Arrange
var testObject = new TestDataModel { Name = "Test", Value = 42 };

var dataType = CreateDataType(["application/xml"]);

// Act
var (data, _) = _sut.SerializeToStorage(testObject, dataType);

// Assert
var xml = System.Text.Encoding.UTF8.GetString(data.Span);

xml.Should()
.Be(
"<?xml version=\"1.0\" encoding=\"utf-8\"?><TestDataModel xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\"><Name>Test</Name><Value>42</Value></TestDataModel>"
);
}

[Fact]
public void SerializeToStorage_ThrowsOnUnsupportedContentType()
{
// Arrange
var testObject = new TestDataModel { Name = "Test", Value = 42 };

var dataType = CreateDataType(["application/unsupported"]);

// Act
var act = () => _sut.SerializeToStorage(testObject, dataType);

// Assert
act.Should().Throw<InvalidOperationException>();
}

[Fact]
public void SerializeToStorage_ThrowsOnMismatchingModelType()
{
// Arrange
var testObject = new TestDataModel { Name = "Test", Value = 42 };

var mismatchingDataType = new DataType()
{
Id = "mismatching",
AppLogic = new ApplicationLogic() { ClassRef = _otherClassRef },
AllowedContentTypes = ["application/json"],
};

// Act
var act = () => _sut.SerializeToStorage(testObject, mismatchingDataType);

// Assert
act.Should().Throw<InvalidOperationException>();
}

[Fact]
public void SerializeToStorage_MultipleAllowedContentTypes_PicksTheFirstOne()
{
// Arrange
var testObject = new TestDataModel { Name = "Test", Value = 42 };

var dataType = CreateDataType(["application/json", "application/xml"]);

// Act
var (_, outputContentType) = _sut.SerializeToStorage(testObject, dataType);

// Assert
outputContentType.Should().Be("application/json");
}

[Fact]
public void DeserializeFromStorage_ThrowsOnMismatchingModelType()
{
// Arrange
var testObject = new TestDataModel { Name = "Test", Value = 42 };
var data = _sut.SerializeToJson(testObject);

var mismatchingDataType = new DataType()
{
Id = "mismatching",
AppLogic = new ApplicationLogic() { ClassRef = _otherClassRef },
};

// Act
var act = () => _sut.DeserializeFromStorage(data.Span, mismatchingDataType);

// Assert
act.Should().Throw<InvalidOperationException>();
}

[Fact]
public void DeserializeFromStorage_DeserializesJson()
{
// Arrange
var testObject = new TestDataModel { Name = "Test", Value = 42 };
var data = _sut.SerializeToJson(testObject);

var dataType = CreateDataType(["application/json"]);

// Act
var result = _sut.DeserializeFromStorage(data.Span, dataType, "application/json");

// Assert
result.Should().BeOfType<TestDataModel>();
var model = (TestDataModel)result;
model.Name.Should().Be("Test");
model.Value.Should().Be(42);
}

[Fact]
public void DeserializeFromStorage_DeserializesXml()
{
// Arrange
var testObject = new TestDataModel { Name = "Test", Value = 42 };
var data = _sut.SerializeToXml(testObject);

var dataType = CreateDataType(["application/xml"]);

// Act
var result = _sut.DeserializeFromStorage(data.Span, dataType, "application/xml");

// Assert
result.Should().BeOfType<TestDataModel>();
var model = (TestDataModel)result;
model.Name.Should().Be("Test");
model.Value.Should().Be(42);
}

[Fact]
public void DeserializeFromStorage_WithDefaultContentType_DeserializesXml()
{
// Arrange
var testObject = new TestDataModel { Name = "Test", Value = 42 };
var data = _sut.SerializeToXml(testObject);

var dataType = CreateDataType(["application/xml"]);

// Act
var result = _sut.DeserializeFromStorage(data.Span, dataType);

// Assert
result.Should().BeOfType<TestDataModel>();
var model = (TestDataModel)result;
model.Name.Should().Be("Test");
model.Value.Should().Be(42);
}

private static DataType CreateDataType(List<string> allowedContentTypes)
{
return new DataType()
{
AppLogic = new ApplicationLogic() { ClassRef = _testClassRef },
AllowedContentTypes = allowedContentTypes,
};
}

public record SomeOtherType { }

public record TestDataModel
{
public string Name { get; set; } = string.Empty;
public int Value { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2229,7 +2229,7 @@ namespace Altinn.App.Core.Helpers.Serialization
public class ModelSerializationService
{
public ModelSerializationService(Altinn.App.Core.Internal.AppModel.IAppModel appModel, Altinn.App.Core.Features.Telemetry? telemetry = null) { }
public object DeserializeFromStorage(System.ReadOnlySpan<byte> data, Altinn.Platform.Storage.Interface.Models.DataType dataType) { }
public object DeserializeFromStorage(System.ReadOnlySpan<byte> data, Altinn.Platform.Storage.Interface.Models.DataType dataType, string? contentType = "application/xml") { }
public object DeserializeJson(System.ReadOnlySpan<byte> data, System.Type modelType) { }
public System.Threading.Tasks.Task<Altinn.App.Core.Models.Result.ServiceResult<object, Microsoft.AspNetCore.Mvc.ProblemDetails>> DeserializeSingleFromStream(System.IO.Stream body, string? contentType, Altinn.Platform.Storage.Interface.Models.DataType dataType) { }
public object DeserializeXml(System.ReadOnlySpan<byte> data, System.Type modelType) { }
Expand Down
Loading