Skip to content
Open
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
28fb6c6
Stream Correspondence attachments to support large file upload
Ceredron Aug 4, 2025
c8d5744
Fix build
Ceredron Aug 4, 2025
1d1d245
Fix build
Ceredron Aug 4, 2025
0a37109
Revert "Fix build"
Ceredron Aug 4, 2025
6fc12c1
Revert "Fix build"
Ceredron Aug 4, 2025
6f1e076
Fix tests
Ceredron Aug 5, 2025
29fb277
Typo
Ceredron Aug 5, 2025
eb5cb3a
Fix more tests
Ceredron Aug 5, 2025
c8d453d
Merge branch 'main' into feat/streamed-correspondence-upload
Ceredron Aug 5, 2025
fb8f85a
Formatting
Ceredron Aug 5, 2025
61805b8
Merge branch 'feat/streamed-correspondence-upload' of https://github.…
Ceredron Aug 5, 2025
d3a6ad1
Break API
Ceredron Aug 5, 2025
33dcf0b
Expanded interface to include stream implementation
Ceredron Aug 5, 2025
4af8625
Break
Ceredron Aug 5, 2025
2dccc09
Tempfix for testing streaming response
Ceredron Aug 5, 2025
ad99a4d
Only stream when necessary
Ceredron Aug 5, 2025
96211e1
"Non-breaking" version
Ceredron Aug 5, 2025
55b8fde
Fix build
Ceredron Aug 5, 2025
4165255
Bang some tests
Ceredron Aug 5, 2025
e6a0504
Re-factor to try to break less
Ceredron Aug 6, 2025
460e830
Use ResponseStreamWrapper to ensure correct disposal of response mess…
Ceredron Aug 6, 2025
7c7031d
Xml docs
Ceredron Aug 6, 2025
cc5de34
Streams needs to be suffixed with Stream
Ceredron Aug 6, 2025
8821d7b
Small fixes
Ceredron Aug 6, 2025
f30c680
More fix
Ceredron Aug 6, 2025
574f29a
Internalize 'ResponseWrapperStream', disposal, extend XML docs for c…
martinothamar Aug 6, 2025
dbd86ab
Experiment to use REST instead of Form endpoints
Ceredron Aug 6, 2025
723e980
Formatting etc
Ceredron Aug 6, 2025
527bf2c
API for experiment
Ceredron Aug 6, 2025
351cddd
Revert "API for experiment"
Ceredron Aug 6, 2025
dc7485a
Revert "Formatting etc"
Ceredron Aug 6, 2025
351ad4f
Revert "Experiment to use REST instead of Form endpoints"
Ceredron Aug 6, 2025
d600d0a
Merge branch 'main' into feat/streamed-correspondence-upload
martinothamar Aug 7, 2025
0379ca8
New experiment
Ceredron Aug 7, 2025
f8ae91f
Merge branch 'feat/streamed-correspondence-upload' of https://github.…
Ceredron Aug 7, 2025
2204363
Remove usings
Ceredron Aug 7, 2025
29a992a
Revert "Remove usings"
Ceredron Aug 7, 2025
d41e3f5
Revert "New experiment"
Ceredron Aug 7, 2025
49cac69
Disposals, update integration test snapshots
martinothamar Aug 7, 2025
1561d72
Merge branch 'main' into feat/streamed-correspondence-upload
martinothamar Aug 7, 2025
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 @@ -9,7 +9,7 @@ public class CorrespondenceAttachmentBuilder : ICorrespondenceAttachmentBuilder
{
private string? _filename;
private string? _sendersReference;
private ReadOnlyMemory<byte>? _data;
private Stream? _data;
private bool? _isEncrypted;
private CorrespondenceDataLocationType _dataLocationType =
CorrespondenceDataLocationType.ExistingCorrespondenceAttachment;
Expand Down Expand Up @@ -41,6 +41,14 @@ public ICorrespondenceAttachmentBuilderData WithSendersReference(string sendersR
public ICorrespondenceAttachmentBuilder WithData(ReadOnlyMemory<byte> data)
{
BuilderUtils.NotNullOrEmpty(data, "Data cannot be empty");
var memStream = new MemoryStream(data.ToArray());
_data = memStream;
return this;
}

/// <inheritdoc/>
public ICorrespondenceAttachmentBuilder WithData(Stream data)
{
_data = data;
return this;
}
Expand Down Expand Up @@ -70,7 +78,7 @@ public CorrespondenceAttachment Build()
{
Filename = _filename,
SendersReference = _sendersReference,
Data = _data.Value,
Data = _data,
IsEncrypted = _isEncrypted,
DataLocationType = _dataLocationType,
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,11 @@ CancellationToken cancellationToken
)
{
using HttpClient client = _httpClientFactory.CreateClient();

// Configure HttpClient for large file uploads
client.Timeout = TimeSpan.FromMinutes(30);
client.DefaultRequestHeaders.ExpectContinue = false;

using HttpResponseMessage response = await client.SendAsync(request, cancellationToken);
string responseBody = await response.Content.ReadAsStringAsync(cancellationToken);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public sealed record CorrespondenceAttachment : MultipartCorrespondenceItem
/// <summary>
/// The data content.
/// </summary>
public required ReadOnlyMemory<byte> Data { get; init; }
public required Stream Data { get; init; }

internal void Serialise(MultipartFormDataContent content, int index, string? filenameOverride = null)
{
Expand All @@ -40,7 +40,7 @@ internal void Serialise(MultipartFormDataContent content, int index, string? fil
AddRequired(content, actualFilename, $"{prefix}.Filename");
AddRequired(content, SendersReference, $"{prefix}.SendersReference");
AddRequired(content, DataLocationType.ToString(), $"{prefix}.DataLocationType");
AddRequired(content, Data, "Attachments", actualFilename); // NOTE: No prefix!
AddRequired(content, Data, "Attachments", actualFilename);
AddIfNotNull(content, IsEncrypted?.ToString(), $"{prefix}.IsEncrypted");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,13 @@ string filename
content.Add(new ReadOnlyMemoryContent(data), name, filename);
}

internal static void AddRequired(MultipartFormDataContent content, Stream data, string name, string filename)
{
if (data is null)
throw new CorrespondenceArgumentException($"Required value is missing: {name}");
content.Add(new StreamContent(data), name, filename);
}

internal static void AddIfNotNull(MultipartFormDataContent content, string? value, string name)
{
if (!string.IsNullOrWhiteSpace(value))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ public void Build_WithAllProperties_ShouldReturnValidCorrespondence()
{
Filename = data.attachments[1].filename,
SendersReference = data.attachments[1].sendersReference,
Data = Encoding.UTF8.GetBytes(data.attachments[1].data),
Data = new MemoryStream(Encoding.UTF8.GetBytes(data.attachments[1].data)),
DataLocationType = data.attachments[1].dataLocationType,
IsEncrypted = data.attachments[1].isEncrypted,
}
Expand All @@ -210,7 +210,7 @@ public void Build_WithAllProperties_ShouldReturnValidCorrespondence()
{
Filename = data.attachments[2].filename,
SendersReference = data.attachments[2].sendersReference,
Data = Encoding.UTF8.GetBytes(data.attachments[2].data),
Data = new MemoryStream(Encoding.UTF8.GetBytes(data.attachments[2].data)),
DataLocationType = data.attachments[2].dataLocationType,
IsEncrypted = data.attachments[2].isEncrypted,
},
Expand Down Expand Up @@ -273,10 +273,9 @@ public void Build_WithAllProperties_ShouldReturnValidCorrespondence()
correspondence.Content.Attachments[i].IsEncrypted.Should().Be(data.attachments[i].isEncrypted);
correspondence.Content.Attachments[i].SendersReference.Should().Be(data.attachments[i].sendersReference);
correspondence.Content.Attachments[i].DataLocationType.Should().Be(data.attachments[i].dataLocationType);
Encoding
.UTF8.GetString(correspondence.Content.Attachments[i].Data.Span)
.Should()
.Be(data.attachments[i].data);
var byteArray = new byte[correspondence.Content.Attachments[i].Data.Length];
correspondence.Content.Attachments[i].Data.ReadExactly(byteArray);
Encoding.UTF8.GetString(byteArray).Should().Be(data.attachments[i].data);
}

correspondence.Notification.NotificationTemplate.Should().Be(data.notification.template);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,13 @@ public async Task Serialise_ShouldAddCorrectFields()
{
Filename = "filename-1",
SendersReference = "senders-reference-1",
Data = "data"u8.ToArray(),
Data = new MemoryStream("data"u8.ToArray()),
},
new CorrespondenceAttachment
{
Filename = "filename-2",
SendersReference = "senders-reference-2",
Data = "data"u8.ToArray(),
Data = new MemoryStream("data"u8.ToArray()),
DataLocationType = CorrespondenceDataLocationType.NewCorrespondenceAttachment,
IsEncrypted = true,
},
Expand Down Expand Up @@ -212,13 +212,13 @@ public async Task Serialise_ShouldAddCorrectFields_IsReservedOverridesIgnoreRese
{
Filename = "filename-1",
SendersReference = "senders-reference-1",
Data = "data"u8.ToArray(),
Data = new MemoryStream("data"u8.ToArray()),
},
new CorrespondenceAttachment
{
Filename = "filename-2",
SendersReference = "senders-reference-2",
Data = "data"u8.ToArray(),
Data = new MemoryStream("data"u8.ToArray()),
DataLocationType = CorrespondenceDataLocationType.NewCorrespondenceAttachment,
IsEncrypted = true,
},
Expand Down Expand Up @@ -376,13 +376,13 @@ public async Task Serialise_ShouldHandleClashingFilenames(string clashingFilenam
{
Filename = clashingFilename,
SendersReference = "senders-reference-1",
Data = Encoding.UTF8.GetBytes("data-1"),
Data = new MemoryStream(Encoding.UTF8.GetBytes("data-1")),
},
new CorrespondenceAttachment
{
Filename = clashingFilename,
SendersReference = "senders-reference-2",
Data = Encoding.UTF8.GetBytes("data-2"),
Data = new MemoryStream(Encoding.UTF8.GetBytes("data-2")),
},
],
},
Expand All @@ -407,19 +407,19 @@ public void Serialise_ClashingFilenames_ShouldUseReferenceComparison()
{
Filename = "filename",
SendersReference = "senders-reference",
Data = data,
Data = new MemoryStream(data.ToArray()),
},
new CorrespondenceAttachment
{
Filename = "filename",
SendersReference = "senders-reference",
Data = data,
Data = new MemoryStream(data.ToArray()),
},
new CorrespondenceAttachment
{
Filename = "filename",
SendersReference = "senders-reference",
Data = data,
Data = new MemoryStream(data.ToArray()),
},
];
var clonedAttachment = identicalAttachments[^1];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,13 @@ public async Task Serialise_ShouldAddCorrectFields()
{
Filename = "filename-1",
SendersReference = "senders-reference-1",
Data = "data"u8.ToArray(),
Data = new MemoryStream("data"u8.ToArray()),
},
new CorrespondenceAttachment
{
Filename = "filename-2",
SendersReference = "senders-reference-2",
Data = "data"u8.ToArray(),
Data = new MemoryStream("data"u8.ToArray()),
DataLocationType = CorrespondenceDataLocationType.NewCorrespondenceAttachment,
IsEncrypted = true,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -434,7 +434,9 @@ public async Task GetCorrespondenceAttachments_ReturnsCorrectAttachments()
CorrespondenceAttachment attachment = attachments.First();
Assert.Equal("signed.pdf", attachment.Filename);
Assert.Equal(signedElement.Id, attachment.SendersReference);
Assert.Equal(new byte[] { 1, 2, 3 }, attachment.Data);
var byteArray = new byte[3];
attachment.Data.ReadExactly(byteArray);
Assert.Equal(new byte[] { 1, 2, 3 }, byteArray);
}

[Fact]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,7 @@ namespace Altinn.App.Core.Features.Correspondence.Builder
public class CorrespondenceAttachmentBuilder : Altinn.App.Core.Features.Correspondence.Builder.ICorrespondenceAttachmentBuilder, Altinn.App.Core.Features.Correspondence.Builder.ICorrespondenceAttachmentBuilderData, Altinn.App.Core.Features.Correspondence.Builder.ICorrespondenceAttachmentBuilderFilename, Altinn.App.Core.Features.Correspondence.Builder.ICorrespondenceAttachmentBuilderSendersReference
{
public Altinn.App.Core.Features.Correspondence.Models.CorrespondenceAttachment Build() { }
public Altinn.App.Core.Features.Correspondence.Builder.ICorrespondenceAttachmentBuilder WithData(System.IO.Stream data) { }
public Altinn.App.Core.Features.Correspondence.Builder.ICorrespondenceAttachmentBuilder WithData(System.ReadOnlyMemory<byte> data) { }
public Altinn.App.Core.Features.Correspondence.Builder.ICorrespondenceAttachmentBuilder WithDataLocationType(Altinn.App.Core.Features.Correspondence.Models.CorrespondenceDataLocationType dataLocationType) { }
public Altinn.App.Core.Features.Correspondence.Builder.ICorrespondenceAttachmentBuilderSendersReference WithFilename(string filename) { }
Expand Down Expand Up @@ -724,7 +725,7 @@ namespace Altinn.App.Core.Features.Correspondence.Models
public sealed class CorrespondenceAttachment : Altinn.App.Core.Features.Correspondence.Models.MultipartCorrespondenceItem, System.IEquatable<Altinn.App.Core.Features.Correspondence.Models.CorrespondenceAttachment>
{
public CorrespondenceAttachment() { }
public required System.ReadOnlyMemory<byte> Data { get; init; }
public required System.IO.Stream Data { get; init; }
public Altinn.App.Core.Features.Correspondence.Models.CorrespondenceDataLocationType DataLocationType { get; init; }
public required string Filename { get; init; }
public bool? IsEncrypted { get; init; }
Expand Down
Loading