From aff5a445d9c4c528819f328d30e447425ba222e3 Mon Sep 17 00:00:00 2001 From: Joe Mayo Date: Mon, 13 Apr 2026 17:14:33 -0700 Subject: [PATCH] - Improve async APIs - add CancellationToken - add more async APIs Also: - improve service registration methods. - PackFromMsappReferenceFileAsync now accepts `enableLoadFromYaml` argument that is required in order to enable this scenario. --- .../MsappPacking/MsappPackingServiceTests.cs | 22 +++--- src/Persistence/Compression/PaArchive.cs | 18 ++++- src/Persistence/Compression/PaArchiveEntry.cs | 15 ++++ .../PaArchiveExtensions.ExtractAsync.cs | 23 +++++-- .../Compression/PaArchiveExtensions.cs | 68 +++++++++++++++---- .../MsApp/MsAppServiceCollectionExtensions.cs | 3 +- .../MsappPacking/MsappPackingService.cs | 45 +++++++----- ...MsappPackingServiceCollectionExtensions.cs | 12 ++-- .../MsappReferenceArchiveFactory.cs | 8 +-- 9 files changed, 155 insertions(+), 59 deletions(-) diff --git a/src/Persistence.Tests/MsappPacking/MsappPackingServiceTests.cs b/src/Persistence.Tests/MsappPacking/MsappPackingServiceTests.cs index 57ce010c..900f2bd8 100644 --- a/src/Persistence.Tests/MsappPacking/MsappPackingServiceTests.cs +++ b/src/Persistence.Tests/MsappPacking/MsappPackingServiceTests.cs @@ -15,6 +15,7 @@ namespace Persistence.Tests.MsappPacking; [TestClass] +[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "MSTEST0049:Flow TestContext.CancellationToken to async operations", Justification = "")] public class MsappPackingServiceTests : TestBase { private const string AlmTestApp_asManyEntitiesAsPossible = "AlmTestApp-asManyEntitiesAsPossible.msapp"; @@ -127,10 +128,12 @@ static MsappArchive OpenMsappWithHeader(string headerFileName) } [TestMethod] - public async Task PackFromMsappReferenceFile_RoundTrip_ProducesSameEntries() + [DataRow(false)] + [DataRow(true)] + public async Task PackFromMsappReferenceFile_RoundTrip_ProducesSameEntries(bool enableLoadFromYaml) { // Arrange - var testDir = CreateTestOutputFolder(ensureEmpty: true); + var testDir = CreateTestCaseOutputFolder($"enableLoadFromYaml{enableLoadFromYaml}", ensureEmpty: true); var unpackedDir = Path.Combine(testDir, "unpacked"); var repackedMsappPath = Path.Combine(testDir, "repacked.msapp"); var msappPath = Path.Combine("_TestData", "AlmApps", AlmTestApp_asManyEntitiesAsPossible); @@ -139,7 +142,8 @@ public async Task PackFromMsappReferenceFile_RoundTrip_ProducesSameEntries() // Act: unpack then pack await service.UnpackToDirectoryAsync(msappPath, unpackedDir); var msaprPath = Path.Combine(unpackedDir, AlmTestAppMsaprName); - service.PackFromMsappReferenceFile(msaprPath, repackedMsappPath, TestPackingClient); + await service.PackFromMsappReferenceFileAsync(msaprPath, repackedMsappPath, TestPackingClient + , enableLoadFromYaml: enableLoadFromYaml); // Assert: output file exists File.Exists(repackedMsappPath).Should().BeTrue("the repacked .msapp file should be created"); @@ -165,7 +169,7 @@ public async Task PackFromMsappReferenceFile_RoundTrip_ProducesSameEntries() .Which.DeserializeAsJson(MsappSerialization.PackedJsonSerializeOptions); packedJson.PackedStructureVersion.Should().Be(PackedJson.CurrentPackedStructureVersion); packedJson.LastPackedDateTimeUtc.Should().NotBeNull(); - packedJson.LoadConfiguration.LoadFromYaml.Should().BeTrue("PaYamlSourceCode was unpacked"); + packedJson.LoadConfiguration.LoadFromYaml.Should().Be(enableLoadFromYaml); packedJson.PackingClient.Should().BeEquivalentTo(TestPackingClient); } @@ -186,8 +190,8 @@ public async Task PackFromMsappReferenceFile_ThrowsWhenOutputExists_AndOverwrite File.WriteAllText(outputMsappPath, "existing content"); // Act & Assert - FluentActions.Invoking(() => service.PackFromMsappReferenceFile(msaprPath, outputMsappPath, TestPackingClient, overwriteOutput: false)) - .Should().Throw() + await FluentActions.Invoking(() => service.PackFromMsappReferenceFileAsync(msaprPath, outputMsappPath, TestPackingClient, overwriteOutput: false)) + .Should().ThrowAsync() .WithMessage($"*'{outputMsappPath}'*"); } @@ -208,7 +212,7 @@ public async Task PackFromMsappReferenceFile_Overwrites_WhenOverwriteIsTrue() File.WriteAllText(outputMsappPath, "existing content"); // Act: should not throw - service.PackFromMsappReferenceFile(msaprPath, outputMsappPath, TestPackingClient, overwriteOutput: true); + await service.PackFromMsappReferenceFileAsync(msaprPath, outputMsappPath, TestPackingClient, overwriteOutput: true); // Assert: the file was overwritten with a valid msapp using var msapp = MsappArchiveFactory.Default.Open(outputMsappPath); @@ -239,7 +243,7 @@ public async Task PackFromMsappReferenceFile_PreservesNonAsciiSrcFileNames() // Act var msaprPath = Path.Combine(unpackedDir, AlmTestAppMsaprName); - service.PackFromMsappReferenceFile(msaprPath, repackedMsappPath, TestPackingClient); + await service.PackFromMsappReferenceFileAsync(msaprPath, repackedMsappPath, TestPackingClient); // Assert: each non-ASCII entry name is preserved verbatim in the packed msapp using var repackedMsapp = MsappArchiveFactory.Default.Open(repackedMsappPath); @@ -269,7 +273,7 @@ public async Task PackFromMsappReferenceFile_IgnoresNonPaYamlFileInSrc() var msaprPath = Path.Combine(unpackedDir, AlmTestAppMsaprName); // Act: should not throw; behavior is Ignore - service.PackFromMsappReferenceFile(msaprPath, outputMsappPath, TestPackingClient); + await service.PackFromMsappReferenceFileAsync(msaprPath, outputMsappPath, TestPackingClient); // Assert: notes.txt is NOT present in the output msapp using var repackedMsapp = MsappArchiveFactory.Default.Open(outputMsappPath); diff --git a/src/Persistence/Compression/PaArchive.cs b/src/Persistence/Compression/PaArchive.cs index b259ea01..8e213c42 100644 --- a/src/Persistence/Compression/PaArchive.cs +++ b/src/Persistence/Compression/PaArchive.cs @@ -244,6 +244,7 @@ public IEnumerable GetEntriesInDirectory(PaArchivePath directory } } + [Obsolete($"Use {nameof(AddEntryFromJsonAsync)} instead.")] public void AddEntryFromJson(string fullName, T value, JsonSerializerOptions serializerOptions) { var entry = CreateEntry(fullName); @@ -251,19 +252,30 @@ public void AddEntryFromJson(string fullName, T value, JsonSerializerOptions JsonSerializer.Serialize(stream, value, serializerOptions); } - public void AddEntryFrom(string fullName, PaArchiveEntry sourceEntry) => AddEntryFrom(PaArchivePath.ParseArgument(fullName), sourceEntry); + public async Task AddEntryFromJsonAsync(string fullName, T value, JsonSerializerOptions serializerOptions, CancellationToken cancellationToken = default) + { + var entry = CreateEntry(fullName); + using var stream = entry.Open(); + await JsonSerializer.SerializeAsync(stream, value, serializerOptions, cancellationToken).ConfigureAwait(false); + } + + public async Task AddEntryFromAsync(string fullName, PaArchiveEntry sourceEntry, CancellationToken cancellationToken = default) + { + await AddEntryFromAsync(PaArchivePath.ParseArgument(fullName), sourceEntry, cancellationToken).ConfigureAwait(false); + } - public void AddEntryFrom(PaArchivePath entryPath, PaArchiveEntry sourceEntry) + public async Task AddEntryFromAsync(PaArchivePath entryPath, PaArchiveEntry sourceEntry, CancellationToken cancellationToken = default) { if (sourceEntry.PaArchive.InnerZipArchive == InnerZipArchive) { throw new ArgumentException($"The {nameof(sourceEntry)} can not be from the same archive instance.", nameof(sourceEntry)); } + cancellationToken.ThrowIfCancellationRequested(); var newEntry = CreateEntry(entryPath); using var srcStream = sourceEntry.Open(); using var destStream = newEntry.Open(); - srcStream.CopyTo(destStream); + await srcStream.CopyToAsync(destStream, cancellationToken).ConfigureAwait(false); } #region IDisposable diff --git a/src/Persistence/Compression/PaArchiveEntry.cs b/src/Persistence/Compression/PaArchiveEntry.cs index 934bea89..16e0434d 100644 --- a/src/Persistence/Compression/PaArchiveEntry.cs +++ b/src/Persistence/Compression/PaArchiveEntry.cs @@ -66,6 +66,21 @@ internal PaArchiveEntry(PaArchive paArchive, ZipArchiveEntry zipEntry, PaArchive /// A Stream that represents the contents of the entry. public Stream Open() => ZipEntry.Open(); + /// + /// Asynchronously opens the entry. + /// See additional docs for . + /// + /// A Stream that represents the contents of the entry. + public async Task OpenAsync(CancellationToken cancellationToken = default) + { +#if NET10_0_OR_GREATER + return await ZipEntry.OpenAsync(cancellationToken).ConfigureAwait(false); +#else + cancellationToken.ThrowIfCancellationRequested(); + return Open(); +#endif + } + /// /// Deletes the entry from the . /// diff --git a/src/Persistence/Compression/PaArchiveExtensions.ExtractAsync.cs b/src/Persistence/Compression/PaArchiveExtensions.ExtractAsync.cs index 11207c33..de15afd8 100644 --- a/src/Persistence/Compression/PaArchiveExtensions.ExtractAsync.cs +++ b/src/Persistence/Compression/PaArchiveExtensions.ExtractAsync.cs @@ -10,14 +10,15 @@ namespace Microsoft.PowerPlatform.PowerApps.Persistence.Compression; public static partial class PaArchiveExtensions { #if NET10_0_OR_GREATER - public static async Task ExtractToFileAsync(this PaArchiveEntry source, string destinationFileName, bool overwrite = false) + public static async Task ExtractToFileAsync(this PaArchiveEntry source, string destinationFileName, bool overwrite = false, CancellationToken cancellationToken = default) { // .net 10 supports ExtractToFileAsync - await source.ZipEntry.ExtractToFileAsync(destinationFileName, overwrite).ConfigureAwait(false); + await source.ZipEntry.ExtractToFileAsync(destinationFileName, overwrite, cancellationToken).ConfigureAwait(false); } #else - public static ValueTask ExtractToFileAsync(this PaArchiveEntry source, string destinationFileName, bool overwrite = false) + public static ValueTask ExtractToFileAsync(this PaArchiveEntry source, string destinationFileName, bool overwrite = false, CancellationToken cancellationToken = default) { + cancellationToken.ThrowIfCancellationRequested(); source.ZipEntry.ExtractToFile(destinationFileName, overwrite); return ValueTask.CompletedTask; } @@ -42,14 +43,18 @@ public static async Task ExtractToDirectoryAsync(this PaArchive source, string d /// /// This method is protected against ZipSlip attacks. /// - public static async Task ExtractToDirectoryAsync(this IEnumerable entries, string destinationDirectoryName, bool overwrite = false) + public static async Task ExtractToDirectoryAsync( + this IEnumerable entries, + string destinationDirectoryName, + bool overwrite = false, + CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(entries); ArgumentNullException.ThrowIfNull(destinationDirectoryName); foreach (var entry in entries) { - await entry.ExtractRelativeToDirectoryAsync(destinationDirectoryName, overwrite).ConfigureAwait(false); + await entry.ExtractRelativeToDirectoryAsync(destinationDirectoryName, overwrite, cancellationToken).ConfigureAwait(false); } } @@ -61,7 +66,11 @@ public static async Task ExtractToDirectoryAsync(this IEnumerable /// This method is protected against ZipSlip attacks. /// - public static async Task ExtractRelativeToDirectoryAsync(this PaArchiveEntry source, string destinationDirectoryName, bool overwrite = false) + public static async Task ExtractRelativeToDirectoryAsync( + this PaArchiveEntry source, + string destinationDirectoryName, + bool overwrite = false, + CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(source); ArgumentNullException.ThrowIfNull(destinationDirectoryName); @@ -70,6 +79,6 @@ public static async Task ExtractRelativeToDirectoryAsync(this PaArchiveEntry sou // PaArchiveEntry's should always only ever represent a file Directory.CreateDirectory(Path.GetDirectoryName(fileDestinationPath)!); - await source.ExtractToFileAsync(fileDestinationPath, overwrite: overwrite).ConfigureAwait(false); + await source.ExtractToFileAsync(fileDestinationPath, overwrite: overwrite, cancellationToken).ConfigureAwait(false); } } diff --git a/src/Persistence/Compression/PaArchiveExtensions.cs b/src/Persistence/Compression/PaArchiveExtensions.cs index b59403b6..185d47cb 100644 --- a/src/Persistence/Compression/PaArchiveExtensions.cs +++ b/src/Persistence/Compression/PaArchiveExtensions.cs @@ -9,32 +9,70 @@ namespace Microsoft.PowerPlatform.PowerApps.Persistence.Compression; public static partial class PaArchiveExtensions { + /// + /// Reads the contents of the entry as JSON and deserializes it to an instance of type using . + /// + /// The deserialized object. + /// The json represents a value of null, or an exception occured during deserialization. public static T DeserializeAsJson(this PaArchiveEntry entry, JsonSerializerOptions serializerOptions) where T : notnull { - // TODO: Create a new exception 'PaArchiveException' set of exceptions to target archive-only type errors. - // For now though, calling code currently expects PersistenceLibraryException so we'll keep it. - // maybe 'PaArchiveException' can inherit from PersistenceLibraryException, and we can use it for more specific error codes and to avoid confusion with other types of PersistenceLibraryExceptions that are not archive related. + using var entryStream = entry.Open(); + + try + { + return JsonSerializer.Deserialize(entryStream, serializerOptions) + ?? throw CreatePersistenceExceptionForNullJsonEntry(entry); + } + catch (JsonException ex) + { + throw CreatePersistenceExceptionFrom(ex, entry); + } + } + + /// + /// Asynchronously reads the contents of the entry as JSON and deserializes it to an instance of type using . + /// + /// The deserialized object. + /// The json represents a value of null, or an exception occured during deserialization. + public static async Task DeserializeAsJsonAsync(this PaArchiveEntry entry, JsonSerializerOptions serializerOptions, CancellationToken cancellationToken = default) + where T : notnull + { + using var entryStream = await entry.OpenAsync(cancellationToken).ConfigureAwait(false); + try { - return JsonSerializer.Deserialize(entry.Open(), serializerOptions) - ?? throw new PersistenceLibraryException(PersistenceErrorCode.PaArchiveEntryDeserializedToJsonNull, "Deserialization of json file resulted in null object.") - { - MsappEntryFullPath = entry.FullName, - }; + return await JsonSerializer.DeserializeAsync(entryStream, serializerOptions, cancellationToken).ConfigureAwait(false) + ?? throw CreatePersistenceExceptionForNullJsonEntry(entry); } catch (JsonException ex) { - throw new PersistenceLibraryException(PersistenceErrorCode.PaArchiveEntryDeserializedToJsonFailed, $"Failed to deserialize json file to an instance of {typeof(T).Name}.", ex) - { - MsappEntryFullPath = entry.FullName, - LineNumber = ex.LineNumber, - Column = ex.BytePositionInLine, - JsonPath = ex.Path, - }; + throw CreatePersistenceExceptionFrom(ex, entry); } } + private static PersistenceLibraryException CreatePersistenceExceptionForNullJsonEntry(PaArchiveEntry entry) + { + return new(PersistenceErrorCode.PaArchiveEntryDeserializedToJsonNull, "Deserialization of json file resulted in null object.") + { + MsappEntryFullPath = entry.FullName, + }; + } + + private static PersistenceLibraryException CreatePersistenceExceptionFrom(JsonException ex, PaArchiveEntry entry) where T : notnull + { + // TODO: Create a new exception 'PaArchiveException' set of exceptions to target archive-only type errors. + // For now though, calling code currently expects PersistenceLibraryException so we'll keep it. + // maybe 'PaArchiveException' can inherit from PersistenceLibraryException, and we can use it for more specific error codes and to avoid confusion with other types of PersistenceLibraryExceptions that are not archive related. + return new(PersistenceErrorCode.PaArchiveEntryDeserializedToJsonFailed, $"Failed to deserialize json file to an instance of {typeof(T).Name}.", ex) + { + MsappEntryFullPath = entry.FullName, + LineNumber = ex.LineNumber, + Column = ex.BytePositionInLine, + JsonPath = ex.Path, + }; + } + /// /// Uses to compute a hash of the contents of the entry. /// diff --git a/src/Persistence/MsApp/MsAppServiceCollectionExtensions.cs b/src/Persistence/MsApp/MsAppServiceCollectionExtensions.cs index 69ad5fe1..34236d48 100644 --- a/src/Persistence/MsApp/MsAppServiceCollectionExtensions.cs +++ b/src/Persistence/MsApp/MsAppServiceCollectionExtensions.cs @@ -4,6 +4,7 @@ using System.IO.Compression; using System.Text; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Logging; namespace Microsoft.PowerPlatform.PowerApps.Persistence.MsApp; @@ -16,6 +17,6 @@ public static class MsAppServiceCollectionExtensions /// the services collection instance. public static void AddMsappArchiveFactory(this IServiceCollection services) { - services.AddSingleton(); + services.TryAddSingleton(); } } diff --git a/src/Persistence/MsappPacking/MsappPackingService.cs b/src/Persistence/MsappPacking/MsappPackingService.cs index ae611fc5..50128162 100644 --- a/src/Persistence/MsappPacking/MsappPackingService.cs +++ b/src/Persistence/MsappPacking/MsappPackingService.cs @@ -38,7 +38,8 @@ public async Task UnpackToDirectoryAsync( string msappPath, string outputDirectory, bool overwriteOutput = false, - UnpackedConfiguration? unpackedConfig = null) + UnpackedConfiguration? unpackedConfig = null, + CancellationToken cancellationToken = default) { ArgumentException.ThrowIfNullOrWhiteSpace(msappPath); ArgumentException.ThrowIfNullOrWhiteSpace(outputDirectory); @@ -103,7 +104,7 @@ public async Task UnpackToDirectoryAsync( // Create/overwite .msapr Directory.CreateDirectory(outputDirectory); - using var msaprArchive = _msappReferenceFactory.CreateNew(msaprPath, CreateMsaprHeaderJson(unpackedConfig), overwrite: overwriteOutput); + using var msaprArchive = await _msappReferenceFactory.CreateNewAsync(msaprPath, CreateMsaprHeaderJson(unpackedConfig), overwrite: overwriteOutput, cancellationToken).ConfigureAwait(false); // Perform unpack instructions on msapp entries var extractedCount = 0; @@ -112,13 +113,13 @@ public async Task UnpackToDirectoryAsync( { if (entryInstruction.InstructionType is MsappUnpackInstructionType.UnpackToRelativeDirectory) { - await entryInstruction.MsappEntry.ExtractRelativeToDirectoryAsync(outputDirectory, overwrite: overwriteOutput).ConfigureAwait(false); + await entryInstruction.MsappEntry.ExtractRelativeToDirectoryAsync(outputDirectory, overwrite: overwriteOutput, cancellationToken).ConfigureAwait(false); extractedCount++; } else if (entryInstruction.InstructionType is MsappUnpackInstructionType.CopyToMsapr) { Debug.Assert(entryInstruction.CopyToMsaprEntryPath is not null); - msaprArchive.AddEntryFrom(entryInstruction.CopyToMsaprEntryPath, entryInstruction.MsappEntry); + await msaprArchive.AddEntryFromAsync(entryInstruction.CopyToMsaprEntryPath, entryInstruction.MsappEntry, cancellationToken).ConfigureAwait(false); referenceCount++; } } @@ -221,11 +222,13 @@ private static MsappContentType GetContentType(PaArchivePath entryPath) /// /// Information about the client performing the packing. /// Indicates whether to allow overwriting the output if it already exists. - public void PackFromMsappReferenceFile( + public async Task PackFromMsappReferenceFileAsync( string msaprPath, string outputMsappPath, PackedJsonPackingClient? packingClient = null, - bool overwriteOutput = false) + bool overwriteOutput = false, + bool enableLoadFromYaml = false, + CancellationToken cancellationToken = default) { ArgumentException.ThrowIfNullOrWhiteSpace(msaprPath); ArgumentException.ThrowIfNullOrWhiteSpace(outputMsappPath); @@ -256,29 +259,39 @@ public void PackFromMsappReferenceFile( if (instruction.CopyFromMsaprEntry is not null) { - outputMsapp.AddEntryFrom(instruction.MsappEntryPath, instruction.CopyFromMsaprEntry); + await outputMsapp.AddEntryFromAsync(instruction.MsappEntryPath, instruction.CopyFromMsaprEntry, cancellationToken).ConfigureAwait(false); copiedFromMsaprCount++; } else if (instruction.ReadFromFilePath is not null) { var newEntry = outputMsapp.CreateEntry(instruction.MsappEntryPath); using var srcStream = File.OpenRead(instruction.ReadFromFilePath); - using var destStream = newEntry.Open(); - srcStream.CopyTo(destStream); + using var destStream = await newEntry.OpenAsync(cancellationToken).ConfigureAwait(false); + await srcStream.CopyToAsync(destStream, cancellationToken).ConfigureAwait(false); addedFromDiskCount++; } } - outputMsapp.AddEntryFromJson(MsappLayoutConstants.FileNames.Packed, new PackedJson + if (enableLoadFromYaml && !unpackedConfig.EnablesContentType(MsappUnpackableContentType.PaYamlSourceCode)) { - PackedStructureVersion = PackedJson.CurrentPackedStructureVersion, - LastPackedDateTimeUtc = DateTime.UtcNow, - PackingClient = packingClient, - LoadConfiguration = new PackedJsonLoadConfiguration + _logger?.LogWarning("enableLoadFromYaml is set to true, but the unpacked configuration does not indicate that PaYamlSourceCode was unpacked. Ignoring request to load from yaml."); + enableLoadFromYaml = false; + } + + await outputMsapp.AddEntryFromJsonAsync( + MsappLayoutConstants.FileNames.Packed, + new PackedJson { - LoadFromYaml = unpackedConfig.EnablesContentType(MsappUnpackableContentType.PaYamlSourceCode), + PackedStructureVersion = PackedJson.CurrentPackedStructureVersion, + LastPackedDateTimeUtc = DateTime.UtcNow, + PackingClient = packingClient, + LoadConfiguration = new() + { + LoadFromYaml = enableLoadFromYaml, + }, }, - }, MsappSerialization.PackedJsonSerializeOptions); + MsappSerialization.PackedJsonSerializeOptions, + cancellationToken).ConfigureAwait(false); _logger?.LogInformation( "Pack complete. Copied {CopiedFromMsapr} entries from msapr. Added {AddedFromDisk} files from disk. Output: {OutputMsappPath}.", diff --git a/src/Persistence/MsappPacking/MsappPackingServiceCollectionExtensions.cs b/src/Persistence/MsappPacking/MsappPackingServiceCollectionExtensions.cs index f8381aff..df6d4ef3 100644 --- a/src/Persistence/MsappPacking/MsappPackingServiceCollectionExtensions.cs +++ b/src/Persistence/MsappPacking/MsappPackingServiceCollectionExtensions.cs @@ -4,6 +4,8 @@ using System.IO.Compression; using System.Text; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.PowerPlatform.PowerApps.Persistence.MsApp; using Microsoft.PowerPlatform.PowerApps.Persistence.MsappPacking; namespace Microsoft.PowerPlatform.PowerApps.Persistence.MsappPacking; @@ -11,11 +13,13 @@ namespace Microsoft.PowerPlatform.PowerApps.Persistence.MsappPacking; public static class MsappPackingServiceCollectionExtensions { /// - /// Registers the service. + /// Registers the service and its dependencies. /// - /// the services collection instance. - public static void AddMsappReferenceArchiveFactory(this IServiceCollection services) + public static void AddMsappPackingService(this IServiceCollection services) { - services.AddSingleton(); + services.TryAddSingleton(); + // And register dependencies + services.AddMsappArchiveFactory(); + services.TryAddSingleton(); } } diff --git a/src/Persistence/MsappPacking/MsappReferenceArchiveFactory.cs b/src/Persistence/MsappPacking/MsappReferenceArchiveFactory.cs index f53d066f..9e513eab 100644 --- a/src/Persistence/MsappPacking/MsappReferenceArchiveFactory.cs +++ b/src/Persistence/MsappPacking/MsappReferenceArchiveFactory.cs @@ -20,21 +20,21 @@ public class MsappReferenceArchiveFactory(ILogger? _logge /// public static readonly MsappReferenceArchiveFactory Default = new(); - internal MsappReferenceArchive CreateNew(string path, MsaprHeaderJson headerJson, bool overwrite = false) + internal async Task CreateNewAsync(string path, MsaprHeaderJson headerJson, bool overwrite = false, CancellationToken cancellationToken = default) { ArgumentException.ThrowIfNullOrWhiteSpace(path); var fileStream = new FileStream(path, overwrite ? FileMode.Create : FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None); - return CreateNew(fileStream, headerJson, leaveOpen: false); + return await CreateNewAsync(fileStream, headerJson, leaveOpen: false, cancellationToken).ConfigureAwait(false); } - internal MsappReferenceArchive CreateNew(Stream stream, MsaprHeaderJson headerJson, bool leaveOpen = false) + internal async Task CreateNewAsync(Stream stream, MsaprHeaderJson headerJson, bool leaveOpen = false, CancellationToken cancellationToken = default) { var msapr = new MsappReferenceArchive(stream, ZipArchiveMode.Create, leaveOpen, _logger); // The first thing that must exist in an msapp-ref file is the header; just like with an msapp - msapr.AddEntryFromJson(MsaprLayoutConstants.FileNames.MsaprHeader, headerJson, MsaprSerialization.DefaultJsonSerializeOptions); + await msapr.AddEntryFromJsonAsync(MsaprLayoutConstants.FileNames.MsaprHeader, headerJson, MsaprSerialization.DefaultJsonSerializeOptions, cancellationToken).ConfigureAwait(false); return msapr; }