diff --git a/src/All.slnx b/src/All.slnx
index 56b9455c5ed..2ccf0322499 100644
--- a/src/All.slnx
+++ b/src/All.slnx
@@ -330,4 +330,4 @@
-
+
\ No newline at end of file
diff --git a/src/HotChocolate/Core/src/Execution.Abstractions/Execution/OperationRequestBuilderExtensions.cs b/src/HotChocolate/Core/src/Execution.Abstractions/Execution/Extensions/OperationRequestBuilderExtensions.cs
similarity index 100%
rename from src/HotChocolate/Core/src/Execution.Abstractions/Execution/OperationRequestBuilderExtensions.cs
rename to src/HotChocolate/Core/src/Execution.Abstractions/Execution/Extensions/OperationRequestBuilderExtensions.cs
diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/FetchResultStore.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/FetchResultStore.cs
index 7e8b2d76b2d..5c1da4b5368 100644
--- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/FetchResultStore.cs
+++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/FetchResultStore.cs
@@ -238,6 +238,11 @@ public ImmutableArray CreateVariableValueSets(
(next, current) = (current, next);
next.Clear();
+
+ if (current.Count == 0)
+ {
+ return [];
+ }
}
PooledArrayWriter? buffer = null;
diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Packaging/ArchiveSession.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Packaging/ArchiveSession.cs
index df4b3e4fecb..894f4b86282 100644
--- a/src/HotChocolate/Fusion-vnext/src/Fusion.Packaging/ArchiveSession.cs
+++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Packaging/ArchiveSession.cs
@@ -1,3 +1,4 @@
+using System.Buffers;
using System.IO.Compression;
namespace HotChocolate.Fusion.Packaging;
@@ -6,15 +7,17 @@ internal sealed class ArchiveSession : IDisposable
{
private readonly Dictionary _files = [];
private readonly ZipArchive _archive;
+ private readonly FusionArchiveReadOptions _readOptions;
private FusionArchiveMode _mode;
private bool _disposed;
- public ArchiveSession(ZipArchive archive, FusionArchiveMode mode)
+ public ArchiveSession(ZipArchive archive, FusionArchiveMode mode, FusionArchiveReadOptions readOptions)
{
ArgumentNullException.ThrowIfNull(archive);
_archive = archive;
_mode = mode;
+ _readOptions = readOptions;
}
public bool HasUncommittedChanges
@@ -39,7 +42,7 @@ public IEnumerable GetFiles()
return files;
}
- public async Task ExistsAsync(string path, CancellationToken cancellationToken)
+ public async Task ExistsAsync(string path, FileKind kind, CancellationToken cancellationToken)
{
if (_files.TryGetValue(path, out var file))
{
@@ -49,12 +52,7 @@ public async Task ExistsAsync(string path, CancellationToken cancellationT
if (_mode is not FusionArchiveMode.Create && _archive.GetEntry(path) is { } entry)
{
file = FileEntry.Read(path);
-#if NET10_0_OR_GREATER
- await entry.ExtractToFileAsync(file.TempPath, cancellationToken);
-#else
- entry.ExtractToFile(file.TempPath);
- await Task.CompletedTask;
-#endif
+ await ExtractFileAsync(entry, file, GetAllowedSize(kind), cancellationToken);
_files.Add(path, file);
return true;
}
@@ -72,7 +70,7 @@ public bool Exists(string path)
return _mode is not FusionArchiveMode.Create && _archive.GetEntry(path) is not null;
}
- public async Task OpenReadAsync(string path, CancellationToken cancellationToken)
+ public async Task OpenReadAsync(string path, FileKind kind, CancellationToken cancellationToken)
{
if (_files.TryGetValue(path, out var file))
{
@@ -87,12 +85,7 @@ public async Task OpenReadAsync(string path, CancellationToken cancellat
if (_mode is not FusionArchiveMode.Create && _archive.GetEntry(path) is { } entry)
{
file = FileEntry.Read(path);
-#if NET10_0_OR_GREATER
- await entry.ExtractToFileAsync(file.TempPath, cancellationToken);
-#else
- entry.ExtractToFile(file.TempPath);
- await Task.CompletedTask;
-#endif
+ await ExtractFileAsync(entry, file, GetAllowedSize(kind), cancellationToken);
var stream = File.OpenRead(file.TempPath);
_files.Add(path, file);
return stream;
@@ -181,6 +174,43 @@ await _archive.CreateEntryFromFileAsync(
}
}
+ private static async Task ExtractFileAsync(
+ ZipArchiveEntry zipEntry,
+ FileEntry fileEntry,
+ int maxAllowedSize,
+ CancellationToken cancellationToken)
+ {
+ var buffer = ArrayPool.Shared.Rent(4096);
+ var consumed = 0;
+
+ await using var readStream = zipEntry.Open();
+ await using var writeStream = File.Open(fileEntry.TempPath, FileMode.Create, FileAccess.Write);
+
+ int read;
+ while ((read = await readStream.ReadAsync(buffer, cancellationToken)) > 0)
+ {
+ consumed += read;
+
+ if (consumed > maxAllowedSize)
+ {
+ throw new InvalidOperationException(
+ $"File is too large and exceeds the allowed size of {maxAllowedSize}.");
+ }
+
+ await writeStream.WriteAsync(buffer.AsMemory(0, read), cancellationToken);
+ }
+ }
+
+ private int GetAllowedSize(FileKind kind)
+ => kind switch
+ {
+ FileKind.Schema
+ => _readOptions.MaxAllowedSchemaSize,
+ FileKind.Manifest or FileKind.Settings or FileKind.Metadata or FileKind.Signature
+ => _readOptions.MaxAllowedSettingsSize,
+ _ => throw new ArgumentOutOfRangeException(nameof(kind), kind, null)
+ };
+
public void Dispose()
{
if (_disposed)
@@ -188,14 +218,22 @@ public void Dispose()
return;
}
- _disposed = true;
foreach (var file in _files.Values)
{
- if (file.State is not FileState.Deleted)
+ if (file.State is not FileState.Deleted && File.Exists(file.TempPath))
{
- File.Delete(file.TempPath);
+ try
+ {
+ File.Delete(file.TempPath);
+ }
+ catch
+ {
+ // ignore
+ }
}
}
+
+ _disposed = true;
}
private class FileEntry
diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Packaging/FileKind.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Packaging/FileKind.cs
new file mode 100644
index 00000000000..916809e8fe8
--- /dev/null
+++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Packaging/FileKind.cs
@@ -0,0 +1,10 @@
+namespace HotChocolate.Fusion.Packaging;
+
+internal enum FileKind
+{
+ Schema,
+ Settings,
+ Manifest,
+ Metadata,
+ Signature
+}
diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Packaging/FileNames.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Packaging/FileNames.cs
index fba1f758cd0..c73a3e2e22a 100644
--- a/src/HotChocolate/Fusion-vnext/src/Fusion.Packaging/FileNames.cs
+++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Packaging/FileNames.cs
@@ -5,6 +5,7 @@ internal static class FileNames
private const string GatewaySchemaFormat = "gateway/{0}/gateway.graphqls";
private const string GatewaySettingsFormat = "gateway/{0}/gateway-settings.json";
private const string SourceSchemaFormat = "source-schemas/{0}/schema.graphqls";
+ private const string SourceSchemaSettingsFormat = "source-schemas/{0}/schema-settings.json";
public const string ArchiveMetadata = "archive-metadata.json";
public const string CompositionSettings = "composition-settings.json";
@@ -19,4 +20,34 @@ public static string GetGatewaySettingsPath(Version version)
public static string GetSourceSchemaPath(string schemaName)
=> string.Format(SourceSchemaFormat, schemaName);
+
+ public static string GetSourceSchemaSettingsPath(string schemaName)
+ => string.Format(SourceSchemaSettingsFormat, schemaName);
+
+ public static FileKind GetFileKind(string fileName)
+ {
+ switch (Path.GetFileName(fileName))
+ {
+ case "gateway.graphqls":
+ case "schema.graphqls":
+ return FileKind.Schema;
+
+ case "schema-settings.json":
+ case "gateway-settings.json":
+ case "composition-settings.json":
+ return FileKind.Settings;
+
+ case "archive-metadata.json":
+ return FileKind.Metadata;
+
+ case "manifest.json":
+ return FileKind.Manifest;
+
+ case "signature.json":
+ return FileKind.Signature;
+
+ default:
+ return FileKind.Settings;
+ }
+ }
}
diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Packaging/FusionArchive.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Packaging/FusionArchive.cs
index 7f2d38bc389..dcb7f970c8f 100644
--- a/src/HotChocolate/Fusion-vnext/src/Fusion.Packaging/FusionArchive.cs
+++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Packaging/FusionArchive.cs
@@ -26,13 +26,17 @@ public sealed class FusionArchive : IDisposable
private ArchiveMetadata? _metadata;
private bool _disposed;
- private FusionArchive(Stream stream, FusionArchiveMode mode, bool leaveOpen = false)
+ private FusionArchive(
+ Stream stream,
+ FusionArchiveMode mode,
+ bool leaveOpen,
+ FusionArchiveReadOptions options)
{
_stream = stream;
_mode = mode;
_leaveOpen = leaveOpen;
_archive = new ZipArchive(stream, (ZipArchiveMode)mode, leaveOpen);
- _session = new ArchiveSession(_archive, mode);
+ _session = new ArchiveSession(_archive, mode, options);
}
///
@@ -57,7 +61,7 @@ public static FusionArchive Create(string filename)
public static FusionArchive Create(Stream stream, bool leaveOpen = false)
{
ArgumentNullException.ThrowIfNull(stream);
- return new FusionArchive(stream, FusionArchiveMode.Create, leaveOpen);
+ return new FusionArchive(stream, FusionArchiveMode.Create, leaveOpen, FusionArchiveReadOptions.Default);
}
///
@@ -89,15 +93,20 @@ public static FusionArchive Open(
/// The stream containing the archive data.
/// The mode to open the archive in.
/// True to leave the stream open after disposal; otherwise, false.
+ /// The options to use when reading from the archive.
/// A FusionArchive instance opened in the specified mode.
/// Thrown when stream is null.
public static FusionArchive Open(
Stream stream,
FusionArchiveMode mode = FusionArchiveMode.Read,
- bool leaveOpen = false)
+ bool leaveOpen = false,
+ FusionArchiveOptions options = default)
{
ArgumentNullException.ThrowIfNull(stream);
- return new FusionArchive(stream, mode, leaveOpen);
+ var readOptions = new FusionArchiveReadOptions(
+ options.MaxAllowedSchemaSize ?? FusionArchiveReadOptions.Default.MaxAllowedSchemaSize,
+ options.MaxAllowedSettingsSize ?? FusionArchiveReadOptions.Default.MaxAllowedSettingsSize);
+ return new FusionArchive(stream, mode, leaveOpen, readOptions);
}
///
@@ -156,7 +165,7 @@ public async Task SetArchiveMetadataAsync(
return _metadata;
}
- if (!await _session.ExistsAsync(FileNames.ArchiveMetadata, cancellationToken))
+ if (!await _session.ExistsAsync(FileNames.ArchiveMetadata, FileKind.Metadata, cancellationToken))
{
return null;
}
@@ -165,7 +174,10 @@ public async Task SetArchiveMetadataAsync(
try
{
- await using var stream = await _session.OpenReadAsync(FileNames.ArchiveMetadata, cancellationToken);
+ await using var stream = await _session.OpenReadAsync(
+ FileNames.ArchiveMetadata,
+ FileKind.Metadata,
+ cancellationToken);
await stream.CopyToAsync(buffer, cancellationToken);
var metadata = ArchiveMetadataSerializer.Parse(buffer.WrittenMemory);
_metadata = metadata;
@@ -292,51 +304,61 @@ public async Task SetCompositionSettingsAsync(
{
ObjectDisposedException.ThrowIf(_disposed, this);
- if (!await _session.ExistsAsync(FileNames.CompositionSettings, cancellationToken))
+ if (!await _session.ExistsAsync(FileNames.CompositionSettings, FileKind.Settings, cancellationToken))
{
return null;
}
- await using var stream = await _session.OpenReadAsync(FileNames.CompositionSettings, cancellationToken);
+ await using var stream = await _session.OpenReadAsync(
+ FileNames.CompositionSettings,
+ FileKind.Settings,
+ cancellationToken);
return await JsonDocument.ParseAsync(stream, default, cancellationToken);
}
///
- /// Sets the gateway schema for a specific format version.
+ /// Sets the gateway configuration for a specific format version using raw bytes.
/// The version must be declared in the archive metadata before calling this method.
///
/// The gateway schema as a GraphQL schema string.
+ /// The gateway settings as a JSON document.
/// The gateway format version.
/// Token to cancel the operation.
/// Thrown when schema is null or empty.
/// Thrown when version is null.
/// Thrown when the archive has been disposed.
/// Thrown when the archive is read-only, metadata is missing, or version is not declared.
- public async Task SetGatewaySchemaAsync(
+ public async Task SetGatewayConfigurationAsync(
string schema,
+ JsonDocument settings,
Version version,
CancellationToken cancellationToken = default)
{
ArgumentException.ThrowIfNullOrEmpty(schema);
+ ArgumentNullException.ThrowIfNull(settings);
ArgumentNullException.ThrowIfNull(version);
ObjectDisposedException.ThrowIf(_disposed, this);
EnsureMutable();
- await SetGatewaySchemaAsync(Encoding.UTF8.GetBytes(schema), version, cancellationToken);
+ await SetGatewayConfigurationAsync(Encoding.UTF8.GetBytes(schema), settings, version, cancellationToken);
}
///
- /// Sets the gateway schema for a specific format version using raw bytes.
+ /// Sets the gateway configuration for a specific format version using raw bytes.
/// The version must be declared in the archive metadata before calling this method.
///
/// The gateway schema as UTF-8 encoded bytes.
+ /// The gateway settings as a JSON document.
/// The gateway format version.
/// Token to cancel the operation.
/// Thrown when version is null.
/// Thrown when the archive has been disposed.
- /// Thrown when the archive is read-only, metadata is missing, or version is not declared.
- public async Task SetGatewaySchemaAsync(
+ ///
+ /// Thrown when the archive is read-only, metadata is missing, or version is not declared.
+ ///
+ public async Task SetGatewayConfigurationAsync(
ReadOnlyMemory schema,
+ JsonDocument settings,
Version version,
CancellationToken cancellationToken = default)
{
@@ -358,116 +380,30 @@ public async Task SetGatewaySchemaAsync(
"You need to first declare the gateway schema version in the archive metadata.");
}
- await using var stream = _session.OpenWrite(FileNames.GetGatewaySchemaPath(version));
- await stream.WriteAsync(schema, cancellationToken);
- }
-
- ///
- /// Attempts to get a gateway schema with the highest version that is less than or equal to the specified maximum version.
- /// The schema data is written to the provided buffer.
- ///
- /// The maximum version to consider.
- /// The buffer to write the schema data to.
- /// Token to cancel the operation.
- /// A result indicating whether resolution was successful and the actual version used.
- /// Thrown when maxVersion or buffer is null.
- /// Thrown when the archive has been disposed.
- /// Thrown when no supported gateway formats are found.
- public async Task TryGetGatewaySchemaAsync(
- Version maxVersion,
- IBufferWriter buffer,
- CancellationToken cancellationToken = default)
- {
- ArgumentNullException.ThrowIfNull(maxVersion);
- ArgumentNullException.ThrowIfNull(buffer);
- ObjectDisposedException.ThrowIf(_disposed, this);
-
- var metadata = await GetArchiveMetadataAsync(cancellationToken);
- if (metadata?.SupportedGatewayFormats == null || !metadata.SupportedGatewayFormats.Any())
- {
- throw new InvalidOperationException("No supported gateway formats found in archive metadata.");
- }
-
- // we need to find the version that is less than or equal to the maxVersion
- var version = metadata.SupportedGatewayFormats.OrderByDescending(v => v).FirstOrDefault(v => v <= maxVersion);
- if (version == null)
- {
- return new ResolvedGatewaySchemaResult { IsResolved = false, ActualVersion = null };
- }
-
- await using var stream = await _session.OpenReadAsync(
- FileNames.GetGatewaySchemaPath(version),
- cancellationToken);
- await stream.CopyToAsync(buffer, cancellationToken);
- return new ResolvedGatewaySchemaResult { IsResolved = true, ActualVersion = version };
- }
-
- ///
- /// Sets the gateway settings for a specific format version.
- /// The version must be declared in the archive metadata before calling this method.
- ///
- /// The gateway settings as a JSON document.
- /// The gateway format version.
- /// Token to cancel the operation.
- /// Thrown when settings or version is null.
- /// Thrown when the archive has been disposed.
- /// Thrown when the archive is read-only, metadata is missing, or version is not declared.
- public async Task SetGatewaySettingsAsync(
- JsonDocument settings,
- Version version,
- CancellationToken cancellationToken = default)
- {
- ArgumentNullException.ThrowIfNull(settings);
- ArgumentNullException.ThrowIfNull(version);
- ObjectDisposedException.ThrowIf(_disposed, this);
- EnsureMutable();
-
- var metadata = await GetArchiveMetadataAsync(cancellationToken);
-
- if (metadata is null)
- {
- throw new InvalidOperationException(
- "You need to first define the archive metadata.");
- }
-
- if (!metadata.SupportedGatewayFormats.Contains(version))
+ await using (var stream = _session.OpenWrite(FileNames.GetGatewaySchemaPath(version)))
{
- throw new InvalidOperationException(
- "You need to first declare the gateway schema version in the archive metadata.");
+ await stream.WriteAsync(schema, cancellationToken);
}
- Exception? exception = null;
- await using var stream = _session.OpenWrite(FileNames.GetGatewaySettingsPath(version));
- var writer = PipeWriter.Create(stream);
-
- try
+ await using (var stream = _session.OpenWrite(FileNames.GetGatewaySettingsPath(version)))
{
- await using var jsonWriter = new Utf8JsonWriter(writer, new JsonWriterOptions { Indented = true });
+ await using var jsonWriter = new Utf8JsonWriter(stream, new JsonWriterOptions { Indented = true });
settings.WriteTo(jsonWriter);
await jsonWriter.FlushAsync(cancellationToken);
- await writer.FlushAsync(cancellationToken);
- }
- catch (Exception ex)
- {
- exception = ex;
- throw;
- }
- finally
- {
- await writer.CompleteAsync(exception);
}
}
///
- /// Attempts to get gateway settings with the highest version that is less than or equal to the specified maximum version.
+ /// Attempts to get a gateway schema with the highest version that is less
+ /// than or equal to the specified maximum version.
///
/// The maximum version to consider.
/// Token to cancel the operation.
- /// A result indicating whether resolution was successful, the actual version used, and the settings.
- /// Thrown when maxVersion is null.
+ /// A gateway configuration.
+ /// Thrown when maxVersion or buffer is null.
/// Thrown when the archive has been disposed.
/// Thrown when no supported gateway formats are found.
- public async Task TryGetGatewaySettingsAsync(
+ public async Task TryGetGatewayConfigurationAsync(
Version maxVersion,
CancellationToken cancellationToken = default)
{
@@ -484,19 +420,22 @@ public async Task TryGetGatewaySettingsAsync(
var version = metadata.SupportedGatewayFormats.OrderByDescending(v => v).FirstOrDefault(v => v <= maxVersion);
if (version == null)
{
- return new ResolvedGatewaySettingsResult { IsResolved = false, ActualVersion = null, Settings = null };
+ return null;
}
- if (!await _session.ExistsAsync(FileNames.GetGatewaySettingsPath(version), cancellationToken))
+ JsonDocument settings;
+ await using (var stream = await _session.OpenReadAsync(
+ FileNames.GetGatewaySettingsPath(version),
+ FileKind.Settings,
+ cancellationToken))
{
- return new ResolvedGatewaySettingsResult { IsResolved = false, ActualVersion = null, Settings = null };
+ settings = await JsonDocument.ParseAsync(stream, default, cancellationToken);
}
- await using var stream = await _session.OpenReadAsync(
- FileNames.GetGatewaySettingsPath(version),
- cancellationToken);
- var settings = await JsonDocument.ParseAsync(stream, default, cancellationToken);
- return new ResolvedGatewaySettingsResult { IsResolved = true, ActualVersion = version, Settings = settings };
+ return new GatewayConfiguration(OpenReadSchemaAsync, settings, version);
+
+ Task OpenReadSchemaAsync(CancellationToken ct)
+ => _session.OpenReadAsync(FileNames.GetGatewaySchemaPath(version), FileKind.Schema, ct);
}
///
@@ -505,17 +444,20 @@ public async Task TryGetGatewaySettingsAsync(
///
/// The name of the source schema.
/// The source schema as UTF-8 encoded bytes.
+ /// The source schema configuration.
/// Token to cancel the operation.
/// Thrown when schemaName is null, empty, or invalid.
/// Thrown when schema is empty.
/// Thrown when the archive has been disposed.
/// Thrown when the archive is read-only, metadata is missing, or schema name is not declared.
- public async Task SetSourceSchemaAsync(
+ public async Task SetSourceSchemaConfigurationAsync(
string schemaName,
ReadOnlyMemory schema,
+ JsonDocument settings,
CancellationToken cancellationToken = default)
{
ArgumentException.ThrowIfNullOrEmpty(schemaName);
+ ArgumentNullException.ThrowIfNull(settings);
ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(schema.Length, 0);
ObjectDisposedException.ThrowIf(_disposed, this);
@@ -540,39 +482,55 @@ public async Task SetSourceSchemaAsync(
"You need to first declare the source schema in the archive metadata.");
}
- await using var stream = _session.OpenWrite(FileNames.GetSourceSchemaPath(schemaName));
- await stream.WriteAsync(schema, cancellationToken);
+ await using (var stream = _session.OpenWrite(FileNames.GetSourceSchemaPath(schemaName)))
+ {
+ await stream.WriteAsync(schema, cancellationToken);
+ }
+
+ await using (var stream = _session.OpenWrite(FileNames.GetSourceSchemaSettingsPath(schemaName)))
+ {
+ await using var jsonWriter = new Utf8JsonWriter(stream, new JsonWriterOptions { Indented = true });
+ settings.WriteTo(jsonWriter);
+ await jsonWriter.FlushAsync(cancellationToken);
+ }
}
///
- /// Attempts to get a source schema from the archive.
- /// The schema data is written to the provided buffer if found.
+ /// Attempts to get a source schema configuration from the archive.
///
/// The name of the source schema to retrieve.
- /// The buffer to write the schema data to.
/// Token to cancel the operation.
- /// True if the schema was found and retrieved; otherwise, false.
+ /// A source schema configuration.
/// Thrown when schemaName or buffer is null.
/// Thrown when the archive has been disposed.
- public async Task TryGetSourceSchemaAsync(
+ public async Task TryGetSourceSchemaConfigurationAsync(
string schemaName,
- IBufferWriter buffer,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(schemaName);
- ArgumentNullException.ThrowIfNull(buffer);
ObjectDisposedException.ThrowIf(_disposed, this);
- if (!await _session.ExistsAsync(FileNames.GetSourceSchemaPath(schemaName), cancellationToken))
+ if (!await _session.ExistsAsync(
+ FileNames.GetSourceSchemaPath(schemaName),
+ FileKind.Schema,
+ cancellationToken))
{
- return false;
+ return null;
}
- await using var stream = await _session.OpenReadAsync(
- FileNames.GetSourceSchemaPath(schemaName),
- cancellationToken);
- await stream.CopyToAsync(buffer, cancellationToken);
- return true;
+ JsonDocument settings;
+ await using (var stream = await _session.OpenReadAsync(
+ FileNames.GetSourceSchemaSettingsPath(schemaName),
+ FileKind.Settings,
+ cancellationToken))
+ {
+ settings = await JsonDocument.ParseAsync(stream, default, cancellationToken);
+ }
+
+ return new SourceSchemaConfiguration(OpenReadSchemaAsync, settings);
+
+ Task OpenReadSchemaAsync(CancellationToken ct)
+ => _session.OpenReadAsync(FileNames.GetSourceSchemaPath(schemaName), FileKind.Schema, ct);
}
///
@@ -642,8 +600,14 @@ public async Task VerifySignatureAsync(
X509Certificate2 publicKey,
CancellationToken cancellationToken = default)
{
- var manifestExists = await _session.ExistsAsync(FileNames.SignatureManifest, cancellationToken);
- var signatureExists = await _session.ExistsAsync(FileNames.Signature, cancellationToken);
+ var manifestExists = await _session.ExistsAsync(
+ FileNames.SignatureManifest,
+ FileKind.Manifest,
+ cancellationToken);
+ var signatureExists = await _session.ExistsAsync(
+ FileNames.Signature,
+ FileKind.Signature,
+ cancellationToken);
if (!manifestExists || !signatureExists)
{
@@ -657,9 +621,11 @@ public async Task VerifySignatureAsync(
// 1. Load manifest and signature
await using var manifestStream = await _session.OpenReadAsync(
FileNames.SignatureManifest,
+ FileKind.Manifest,
cancellationToken);
await using var signatureStream = await _session.OpenReadAsync(
FileNames.Signature,
+ FileKind.Signature,
cancellationToken);
await manifestStream.CopyToAsync(buffer, cancellationToken);
var manifest = SignatureManifestSerializer.Parse(buffer.WrittenMemory);
@@ -672,12 +638,14 @@ public async Task VerifySignatureAsync(
// 2. Verify file integrity
foreach (var file in manifest.Files.OrderBy(t => t.Key))
{
- if (!await _session.ExistsAsync(file.Key, cancellationToken))
+ var kind = FileNames.GetFileKind(file.Key);
+
+ if (!await _session.ExistsAsync(file.Key, kind, cancellationToken))
{
return SignatureVerificationResult.FilesMissing;
}
- var actualHash = await ComputeFileHashAsync(file.Key, cancellationToken);
+ var actualHash = await ComputeFileHashAsync(file.Key, kind, cancellationToken);
if (!actualHash.Equals(file.Value, StringComparison.OrdinalIgnoreCase))
{
return SignatureVerificationResult.FilesModified;
@@ -726,8 +694,14 @@ public async Task VerifySignatureAsync(
public async Task GetSignatureInfoAsync(
CancellationToken cancellationToken = default)
{
- var manifestExists = await _session.ExistsAsync(FileNames.SignatureManifest, cancellationToken);
- var signatureExists = await _session.ExistsAsync(FileNames.Signature, cancellationToken);
+ var manifestExists = await _session.ExistsAsync(
+ FileNames.SignatureManifest,
+ FileKind.Manifest,
+ cancellationToken);
+ var signatureExists = await _session.ExistsAsync(
+ FileNames.Signature,
+ FileKind.Signature,
+ cancellationToken);
if (!manifestExists || !signatureExists)
{
@@ -740,9 +714,11 @@ public async Task VerifySignatureAsync(
{
await using var manifestStream = await _session.OpenReadAsync(
FileNames.SignatureManifest,
+ FileKind.Manifest,
cancellationToken);
await using var signatureStream = await _session.OpenReadAsync(
FileNames.Signature,
+ FileKind.Signature,
cancellationToken);
await manifestStream.CopyToAsync(buffer, 1024, cancellationToken);
@@ -823,7 +799,8 @@ private async Task GenerateManifestAsync(CancellationToken ca
continue;
}
- files[path] = await ComputeFileHashAsync(path, cancellationToken);
+ var kind = FileNames.GetFileKind(path);
+ files[path] = await ComputeFileHashAsync(path, kind, cancellationToken);
}
var manifest = new SignatureManifest
@@ -846,9 +823,9 @@ private async Task GenerateManifestAsync(CancellationToken ca
}
}
- private async Task ComputeFileHashAsync(string path, CancellationToken cancellationToken)
+ private async Task ComputeFileHashAsync(string path, FileKind kind, CancellationToken cancellationToken)
{
- await using var stream = await _session.OpenReadAsync(path, cancellationToken);
+ await using var stream = await _session.OpenReadAsync(path, kind, cancellationToken);
using var sha256 = SHA256.Create();
var hashBytes = await sha256.ComputeHashAsync(stream, cancellationToken);
#if NET9_0_OR_GREATER
diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Packaging/FusionArchiveOptions.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Packaging/FusionArchiveOptions.cs
new file mode 100644
index 00000000000..eced391031d
--- /dev/null
+++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Packaging/FusionArchiveOptions.cs
@@ -0,0 +1,17 @@
+namespace HotChocolate.Fusion.Packaging;
+
+///
+/// Specifies the options for a Fusion Archive.
+///
+public struct FusionArchiveOptions
+{
+ ///
+ /// Gets or sets the maximum allowed size of a schema in the archive.
+ ///
+ public int? MaxAllowedSchemaSize { get; set; }
+
+ ///
+ /// Gets or sets the maximum allowed size of the settings in the archive.
+ ///
+ public int? MaxAllowedSettingsSize { get; set; }
+}
diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Packaging/FusionArchiveReadOptions.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Packaging/FusionArchiveReadOptions.cs
new file mode 100644
index 00000000000..d26d53e688e
--- /dev/null
+++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Packaging/FusionArchiveReadOptions.cs
@@ -0,0 +1,14 @@
+namespace HotChocolate.Fusion.Packaging;
+
+///
+/// Specifies the read options for a Fusion Archive.
+///
+internal readonly record struct FusionArchiveReadOptions(
+ int MaxAllowedSchemaSize,
+ int MaxAllowedSettingsSize)
+{
+ ///
+ /// Gets the default read options.
+ ///
+ public static FusionArchiveReadOptions Default { get; } = new(50_000_000, 512_000);
+}
diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Packaging/GatewayConfiguration.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Packaging/GatewayConfiguration.cs
new file mode 100644
index 00000000000..cdfed0c3a3e
--- /dev/null
+++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Packaging/GatewayConfiguration.cs
@@ -0,0 +1,53 @@
+using System.Text.Json;
+
+namespace HotChocolate.Fusion.Packaging;
+
+///
+/// Represents a Hot Chocolate Fusion gateway configuration.
+///
+public sealed class GatewayConfiguration : IDisposable
+{
+ private readonly Func> _openReadSchema;
+ private bool _disposed;
+
+ internal GatewayConfiguration(
+ Func> openReadSchema,
+ JsonDocument settings,
+ Version version)
+ {
+ ArgumentNullException.ThrowIfNull(openReadSchema);
+ ArgumentNullException.ThrowIfNull(settings);
+
+ _openReadSchema = openReadSchema;
+ Settings = settings;
+ Version = version;
+ }
+
+ ///
+ /// Gets the version of the gateway configuration.
+ ///
+ public Version Version { get; }
+
+ ///
+ /// Opens the Hot Chocolate Fusion execution schema for reading.
+ ///
+ public Task OpenReadSchemaAsync(CancellationToken cancellationToken = default)
+ => _openReadSchema(cancellationToken);
+
+ ///
+ /// Gets the settings of the gateway configuration.
+ ///
+ public JsonDocument Settings { get; }
+
+ ///
+ /// Disposes the gateway configuration.
+ ///
+ public void Dispose()
+ {
+ if (!_disposed)
+ {
+ Settings.Dispose();
+ _disposed = true;
+ }
+ }
+}
diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Packaging/ResolvedGatewaySchemaResult.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Packaging/ResolvedGatewaySchemaResult.cs
deleted file mode 100644
index 01ec9469479..00000000000
--- a/src/HotChocolate/Fusion-vnext/src/Fusion.Packaging/ResolvedGatewaySchemaResult.cs
+++ /dev/null
@@ -1,40 +0,0 @@
-using System.Diagnostics.CodeAnalysis;
-
-namespace HotChocolate.Fusion.Packaging;
-
-///
-/// Represents the result of attempting to resolve a gateway schema version from a Fusion Archive.
-///
-public readonly struct ResolvedGatewaySchemaResult
-{
- ///
- /// Gets the actual gateway format version that was resolved.
- /// This may be lower than the requested maximum version if a higher version is not available.
- /// Null if no compatible version was found.
- ///
- public required Version? ActualVersion { get; init; }
-
- ///
- /// Gets a value indicating whether a gateway schema version was successfully resolved.
- /// When true, ActualVersion is guaranteed to be non-null.
- ///
- [MemberNotNullWhen(true, nameof(ActualVersion))]
- public required bool IsResolved { get; init; }
-
- ///
- /// Implicitly converts the result to the actual version that was resolved.
- /// Returns null if no version was resolved.
- ///
- /// The result to convert.
- /// The actual version or null.
- public static implicit operator Version?(ResolvedGatewaySchemaResult result)
- => result.ActualVersion;
-
- ///
- /// Implicitly converts the result to a boolean indicating resolution success.
- ///
- /// The result to convert.
- /// True if a schema version was resolved, false otherwise.
- public static implicit operator bool(ResolvedGatewaySchemaResult result)
- => result.IsResolved;
-}
diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Packaging/ResolvedGatewaySettingsResult.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Packaging/ResolvedGatewaySettingsResult.cs
deleted file mode 100644
index 53c67ebadcc..00000000000
--- a/src/HotChocolate/Fusion-vnext/src/Fusion.Packaging/ResolvedGatewaySettingsResult.cs
+++ /dev/null
@@ -1,58 +0,0 @@
-using System.Diagnostics.CodeAnalysis;
-using System.Text.Json;
-
-namespace HotChocolate.Fusion.Packaging;
-
-///
-/// Represents the result of attempting to resolve gateway settings from a Fusion Archive,
-/// including the actual version used and the settings document.
-///
-public readonly struct ResolvedGatewaySettingsResult
-{
- ///
- /// Gets the actual gateway format version that was resolved.
- /// This may be lower than the requested maximum version if a higher version is not available.
- /// Null if no compatible version was found.
- ///
- public required Version? ActualVersion { get; init; }
-
- ///
- /// Gets a value indicating whether gateway settings were successfully resolved.
- /// When true, both ActualVersion and Settings are guaranteed to be non-null.
- ///
- [MemberNotNullWhen(true, nameof(Settings), nameof(ActualVersion))]
- public required bool IsResolved { get; init; }
-
- ///
- /// Gets the resolved gateway settings as a JSON document.
- /// Contains the configuration for transport profiles, source schema endpoints,
- /// and other gateway runtime settings. Null if resolution failed.
- ///
- public required JsonDocument? Settings { get; init; }
-
- ///
- /// Implicitly converts the result to the actual version that was resolved.
- /// Returns null if no version was resolved.
- ///
- /// The result to convert.
- /// The actual version or null.
- public static implicit operator Version?(ResolvedGatewaySettingsResult result)
- => result.ActualVersion;
-
- ///
- /// Implicitly converts the result to a boolean indicating resolution success.
- ///
- /// The result to convert.
- /// True if settings were resolved, false otherwise.
- public static implicit operator bool(ResolvedGatewaySettingsResult result)
- => result.IsResolved;
-
- ///
- /// Implicitly converts the result to the resolved settings JSON document.
- /// Returns null if resolution failed.
- ///
- /// The result to convert.
- /// The settings document or null.
- public static implicit operator JsonDocument?(ResolvedGatewaySettingsResult result)
- => result.Settings;
-}
diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Packaging/SourceSchemaConfiguration.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Packaging/SourceSchemaConfiguration.cs
new file mode 100644
index 00000000000..2a9dc43c8e8
--- /dev/null
+++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Packaging/SourceSchemaConfiguration.cs
@@ -0,0 +1,46 @@
+using System.Text.Json;
+
+namespace HotChocolate.Fusion.Packaging;
+
+///
+/// Represents a Hot Chocolate Fusion source schema configuration.
+///
+public sealed class SourceSchemaConfiguration : IDisposable
+{
+ private readonly Func> _openReadSchema;
+ private bool _disposed;
+
+ internal SourceSchemaConfiguration(
+ Func> openReadSchema,
+ JsonDocument settings)
+ {
+ ArgumentNullException.ThrowIfNull(openReadSchema);
+ ArgumentNullException.ThrowIfNull(settings);
+
+ _openReadSchema = openReadSchema;
+ Settings = settings;
+ }
+
+ ///
+ /// Opens the Hot Chocolate Fusion source schema for reading.
+ ///
+ public Task OpenReadSchemaAsync(CancellationToken cancellationToken = default)
+ => _openReadSchema(cancellationToken);
+
+ ///
+ /// Gets the settings of the source schema configuration.
+ ///
+ public JsonDocument Settings { get; }
+
+ ///
+ /// Disposes the source schema configuration.
+ ///
+ public void Dispose()
+ {
+ if (!_disposed)
+ {
+ Settings.Dispose();
+ _disposed = true;
+ }
+ }
+}
diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.Packaging.Tests/FusionArchiveTests.cs b/src/HotChocolate/Fusion-vnext/test/Fusion.Packaging.Tests/FusionArchiveTests.cs
index 397dc2e3e98..388efa1ac45 100644
--- a/src/HotChocolate/Fusion-vnext/test/Fusion.Packaging.Tests/FusionArchiveTests.cs
+++ b/src/HotChocolate/Fusion-vnext/test/Fusion.Packaging.Tests/FusionArchiveTests.cs
@@ -1,4 +1,3 @@
-using System.Buffers;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;
@@ -147,23 +146,28 @@ public async Task SetGatewaySchema_WithStringContent_StoresCorrectly()
// Arrange
await using var stream = CreateStream();
const string schema = "type Query { hello: String }";
+ var settings = CreateSettingsJson();
var version = new Version("2.0.0");
// Act & Assert
using var archive = FusionArchive.Create(stream, leaveOpen: true);
var metadata = CreateTestMetadata();
await archive.SetArchiveMetadataAsync(metadata);
- await archive.SetGatewaySchemaAsync(schema, version);
+ await archive.SetGatewayConfigurationAsync(schema, settings, version);
// Can read immediately within the same session
- var buffer = new ArrayBufferWriter();
- var result = await archive.TryGetGatewaySchemaAsync(version, buffer);
+ var result = await archive.TryGetGatewayConfigurationAsync(version);
- Assert.True(result.IsResolved);
- Assert.Equal(version, result.ActualVersion);
+ Assert.NotNull(result);
+ Assert.Equal(version, result.Version);
- var retrievedSchema = Encoding.UTF8.GetString(buffer.WrittenSpan);
- Assert.Equal(schema, retrievedSchema);
+ using (var streamReader = new StreamReader(await result.OpenReadSchemaAsync()))
+ {
+ var retrievedSchema = await streamReader.ReadToEndAsync();
+ Assert.Equal(schema, retrievedSchema);
+ }
+
+ result.Dispose();
}
[Fact]
@@ -172,21 +176,28 @@ public async Task SetGatewaySchema_WithByteContent_StoresCorrectly()
// Arrange
await using var stream = CreateStream();
var schema = "type Query { hello: String }"u8.ToArray();
+ var settings = CreateSettingsJson();
var version = new Version("2.0.0");
// Act & Assert
using var archive = FusionArchive.Create(stream, leaveOpen: true);
var metadata = CreateTestMetadata();
await archive.SetArchiveMetadataAsync(metadata);
- await archive.SetGatewaySchemaAsync(schema, version);
+ await archive.SetGatewayConfigurationAsync(schema, settings, version);
// Can read immediately within the same session
- var buffer = new ArrayBufferWriter();
- var result = await archive.TryGetGatewaySchemaAsync(version, buffer);
+ var result = await archive.TryGetGatewayConfigurationAsync(version);
+
+ Assert.NotNull(result);
+ Assert.Equal(version, result.Version);
- Assert.True(result.IsResolved);
- Assert.Equal(version, result.ActualVersion);
- Assert.True(schema.AsSpan().SequenceEqual(buffer.WrittenSpan));
+ using (var streamReader = new StreamReader(await result.OpenReadSchemaAsync()))
+ {
+ var retrievedSchema = await streamReader.ReadToEndAsync();
+ Assert.Equal(Encoding.UTF8.GetString(schema), retrievedSchema);
+ }
+
+ result.Dispose();
}
[Fact]
@@ -198,7 +209,7 @@ public async Task SetGatewaySchema_WithoutMetadata_ThrowsInvalidOperationExcepti
// Act & Assert
using var archive = FusionArchive.Create(stream);
await Assert.ThrowsAsync(
- () => archive.SetGatewaySchemaAsync("schema", new Version("1.0.0")));
+ () => archive.SetGatewayConfigurationAsync("schema", CreateSettingsJson(), new Version("1.0.0")));
}
[Fact]
@@ -213,7 +224,7 @@ public async Task SetGatewaySchema_WithUnsupportedVersion_ThrowsInvalidOperation
await archive.SetArchiveMetadataAsync(metadata);
await Assert.ThrowsAsync(() =>
- archive.SetGatewaySchemaAsync("schema", new Version("3.0.0")));
+ archive.SetGatewayConfigurationAsync("schema", CreateSettingsJson(),new Version("3.0.0")));
}
[Fact]
@@ -230,19 +241,23 @@ public async Task TryGetGatewaySchema_WithCompatibleVersion_ReturnsCorrectVersio
// Act & Assert
using var archive = FusionArchive.Create(stream, leaveOpen: true);
await archive.SetArchiveMetadataAsync(metadata);
- await archive.SetGatewaySchemaAsync("schema v1.0", new Version("1.0.0"));
- await archive.SetGatewaySchemaAsync("schema v2.0", new Version("2.0.0"));
- await archive.SetGatewaySchemaAsync("schema v2.1", new Version("2.1.0"));
+ await archive.SetGatewayConfigurationAsync("schema v1.0", CreateSettingsJson(), new Version("1.0.0"));
+ await archive.SetGatewayConfigurationAsync("schema v2.0", CreateSettingsJson(), new Version("2.0.0"));
+ await archive.SetGatewayConfigurationAsync("schema v2.1", CreateSettingsJson(), new Version("2.1.0"));
// Request max version 2.0.0, should get 2.0.0
- var buffer = new ArrayBufferWriter();
- var result = await archive.TryGetGatewaySchemaAsync(new Version("2.0.0"), buffer);
+ var result = await archive.TryGetGatewayConfigurationAsync(new Version("2.0.0"));
+
+ Assert.NotNull(result);
+ Assert.Equal(new Version("2.0.0"), result.Version);
- Assert.True(result.IsResolved);
- Assert.Equal(new Version("2.0.0"), result.ActualVersion);
+ using (var streamReader = new StreamReader(await result.OpenReadSchemaAsync()))
+ {
+ var retrievedSchema = await streamReader.ReadToEndAsync();
+ Assert.Equal("schema v2.0", retrievedSchema);
+ }
- var schema = Encoding.UTF8.GetString(buffer.WrittenSpan);
- Assert.Equal("schema v2.0", schema);
+ result.Dispose();
}
[Fact]
@@ -260,46 +275,9 @@ public async Task TryGetGatewaySchema_WithIncompatibleVersion_ReturnsFalse()
using var archive = FusionArchive.Create(stream, leaveOpen: true);
await archive.SetArchiveMetadataAsync(metadata);
- var buffer = new ArrayBufferWriter();
- var result = await archive.TryGetGatewaySchemaAsync(new Version("1.0.0"), buffer);
-
- Assert.False(result.IsResolved);
- Assert.Null(result.ActualVersion);
- }
-
- [Fact]
- public async Task SetGatewaySettings_WithValidSettings_StoresCorrectly()
- {
- // Arrange
- await using var stream = CreateStream();
- const string settingsJson =
- """
- {
- "transportProfiles": {
- "http-profile": {
- "type": "graphql-over-http"
- }
- }
- }
- """;
- using var settings = JsonDocument.Parse(settingsJson);
- var version = new Version("2.0.0");
-
- // Act & Assert
- using var archive = FusionArchive.Create(stream, leaveOpen: true);
- var metadata = CreateTestMetadata();
- await archive.SetArchiveMetadataAsync(metadata);
- await archive.SetGatewaySettingsAsync(settings, version);
-
- // Can read immediately within the same session
- var result = await archive.TryGetGatewaySettingsAsync(version);
- Assert.True(result.IsResolved);
- Assert.Equal(version, result.ActualVersion);
- Assert.NotNull(result.Settings);
+ var result = await archive.TryGetGatewayConfigurationAsync(new Version("1.0.0"));
- var transportProfiles = result.Settings.RootElement.GetProperty("transportProfiles");
- Assert.True(transportProfiles.TryGetProperty("http-profile", out var profile));
- Assert.Equal("graphql-over-http", profile.GetProperty("type").GetString());
+ Assert.Null(result);
}
[Fact]
@@ -308,20 +286,23 @@ public async Task SetSourceSchema_WithValidSchema_StoresCorrectly()
// Arrange
await using var stream = CreateStream();
var schemaContent = "type User { id: ID! name: String! }"u8.ToArray();
+ var settings = CreateSettingsJson();
const string schemaName = "user-service";
// Act & Assert
using var archive = FusionArchive.Create(stream, leaveOpen: true);
var metadata = CreateTestMetadata();
await archive.SetArchiveMetadataAsync(metadata);
- await archive.SetSourceSchemaAsync(schemaName, schemaContent);
+ await archive.SetSourceSchemaConfigurationAsync(schemaName, schemaContent, settings);
// Can read immediately within the same session
- var buffer = new ArrayBufferWriter();
- var found = await archive.TryGetSourceSchemaAsync(schemaName, buffer);
+ var found = await archive.TryGetSourceSchemaConfigurationAsync(schemaName);
+
+ Assert.NotNull(found);
- Assert.True(found);
- Assert.True(schemaContent.AsSpan().SequenceEqual(buffer.WrittenSpan));
+ using var streamReader = new StreamReader(await found.OpenReadSchemaAsync());
+ var retrievedSchema = await streamReader.ReadToEndAsync();
+ Assert.Equal(Encoding.UTF8.GetString(schemaContent), retrievedSchema);
}
[Fact]
@@ -336,7 +317,10 @@ public async Task SetSourceSchema_WithInvalidSchemaName_ThrowsArgumentException(
await archive.SetArchiveMetadataAsync(metadata);
await Assert.ThrowsAsync(
- () => archive.SetSourceSchemaAsync("invalid name!", "schema"u8.ToArray()));
+ () => archive.SetSourceSchemaConfigurationAsync(
+ "invalid name!",
+ "schema"u8.ToArray(),
+ CreateSettingsJson()));
}
[Fact]
@@ -355,7 +339,10 @@ public async Task SetSourceSchema_WithUndeclaredSchemaName_ThrowsInvalidOperatio
await archive.SetArchiveMetadataAsync(metadata);
await Assert.ThrowsAsync(
- () => archive.SetSourceSchemaAsync("undeclared-schema", "schema"u8.ToArray()));
+ () => archive.SetSourceSchemaConfigurationAsync(
+ "undeclared-schema",
+ "schema"u8.ToArray(),
+ CreateSettingsJson()));
}
[Fact]
@@ -366,9 +353,8 @@ public async Task TryGetSourceSchema_WithNonExistentSchema_ReturnsFalse()
// Act & Assert
using var archive = FusionArchive.Create(stream, leaveOpen: true);
- var buffer = new ArrayBufferWriter();
- var found = await archive.TryGetSourceSchemaAsync("non-existent", buffer);
- Assert.False(found);
+ var found = await archive.TryGetSourceSchemaConfigurationAsync("non-existent");
+ Assert.Null(found);
}
[Fact]
@@ -382,7 +368,7 @@ public async Task SignArchive_WithValidCertificate_CreatesSignature()
using var archive = FusionArchive.Create(stream, leaveOpen: true);
var metadata = CreateTestMetadata();
await archive.SetArchiveMetadataAsync(metadata);
- await archive.SetGatewaySchemaAsync("schema", new Version("2.0.0"));
+ await archive.SetGatewayConfigurationAsync("schema", CreateSettingsJson(), new Version("2.0.0"));
await archive.SignArchiveAsync(cert);
// Can verify immediately within the same session
@@ -431,7 +417,7 @@ public async Task VerifySignature_WithValidSignature_ReturnsValid()
{
var metadata = CreateTestMetadata();
await archive.SetArchiveMetadataAsync(metadata);
- await archive.SetGatewaySchemaAsync("schema", new Version("2.0.0"));
+ await archive.SetGatewayConfigurationAsync("schema", CreateSettingsJson(), new Version("2.0.0"));
// Sign with private key
await archive.SignArchiveAsync(cert);
@@ -471,7 +457,7 @@ public async Task CommitAndReopen_PersistsChanges()
using (var archive = FusionArchive.Create(stream, leaveOpen: true))
{
await archive.SetArchiveMetadataAsync(metadata);
- await archive.SetGatewaySchemaAsync(schema, new Version("2.0.0"));
+ await archive.SetGatewayConfigurationAsync(schema, CreateSettingsJson(), new Version("2.0.0"));
await archive.CommitAsync();
}
@@ -485,12 +471,14 @@ public async Task CommitAndReopen_PersistsChanges()
metadata.SupportedGatewayFormats.ToArray(),
retrievedMetadata.SupportedGatewayFormats.ToArray());
- var buffer = new ArrayBufferWriter();
- var result = await readArchive.TryGetGatewaySchemaAsync(new Version("2.0.0"), buffer);
- Assert.True(result.IsResolved);
+ var result = await readArchive.TryGetGatewayConfigurationAsync(new Version("2.0.0"));
+ Assert.NotNull(result);
- var retrievedSchema = Encoding.UTF8.GetString(buffer.WrittenSpan);
+ using var streamReader = new StreamReader(await result.OpenReadSchemaAsync());
+ var retrievedSchema = await streamReader.ReadToEndAsync();
Assert.Equal(schema, retrievedSchema);
+
+ result.Dispose();
}
}
@@ -509,7 +497,7 @@ public async Task UpdateMode_CanModifyExistingArchive()
using (var archive = FusionArchive.Create(stream, leaveOpen: true))
{
await archive.SetArchiveMetadataAsync(metadata);
- await archive.SetGatewaySchemaAsync("original schema", new Version("2.0.0"));
+ await archive.SetGatewayConfigurationAsync("original schema", CreateSettingsJson(), new Version("2.0.0"));
await archive.CommitAsync();
}
@@ -517,7 +505,10 @@ public async Task UpdateMode_CanModifyExistingArchive()
stream.Position = 0;
using (var updateArchive = FusionArchive.Open(stream, FusionArchiveMode.Update, leaveOpen: true))
{
- await updateArchive.SetGatewaySchemaAsync("modified schema", new Version("2.0.0"));
+ await updateArchive.SetGatewayConfigurationAsync(
+ "modified schema",
+ CreateSettingsJson(),
+ new Version("2.0.0"));
await updateArchive.CommitAsync();
}
@@ -525,12 +516,14 @@ public async Task UpdateMode_CanModifyExistingArchive()
stream.Position = 0;
using (var readArchive = FusionArchive.Open(stream, leaveOpen: true))
{
- var buffer = new ArrayBufferWriter();
- var result = await readArchive.TryGetGatewaySchemaAsync(new Version("2.0.0"), buffer);
- Assert.True(result.IsResolved);
+ var result = await readArchive.TryGetGatewayConfigurationAsync(new Version("2.0.0"));
+ Assert.NotNull(result);
- var schema = Encoding.UTF8.GetString(buffer.WrittenSpan);
- Assert.Equal("modified schema", schema);
+ using var streamReader = new StreamReader(await result.OpenReadSchemaAsync());
+ var retrievedSchema = await streamReader.ReadToEndAsync();
+ Assert.Equal("modified schema", retrievedSchema);
+
+ result.Dispose();
}
}
@@ -546,16 +539,19 @@ public async Task OverwriteFile_WithinSession_ReplacesContent()
await archive.SetArchiveMetadataAsync(metadata);
// Set schema twice within the same session
- await archive.SetGatewaySchemaAsync("first schema", new Version("2.0.0"));
- await archive.SetGatewaySchemaAsync("second schema", new Version("2.0.0"));
+ await archive.SetGatewayConfigurationAsync("first schema", CreateSettingsJson(), new Version("2.0.0"));
+ await archive.SetGatewayConfigurationAsync("second schema", CreateSettingsJson(), new Version("2.0.0"));
// Should get the last value
- var buffer = new ArrayBufferWriter();
- var result = await archive.TryGetGatewaySchemaAsync(new Version("2.0.0"), buffer);
+ var result = await archive.TryGetGatewayConfigurationAsync(new Version("2.0.0"));
+
+ Assert.NotNull(result);
- Assert.True(result.IsResolved);
- var schema = Encoding.UTF8.GetString(buffer.WrittenSpan);
- Assert.Equal("second schema", schema);
+ using var streamReader = new StreamReader(await result.OpenReadSchemaAsync());
+ var retrievedSchema = await streamReader.ReadToEndAsync();
+ Assert.Equal("second schema", retrievedSchema);
+
+ result.Dispose();
}
[Fact]
@@ -612,7 +608,7 @@ public async Task SetSourceSchema_WithValidSchemaNames_Succeeds(string schemaNam
// Act & Assert - Should not throw
using var archive = FusionArchive.Create(stream, leaveOpen: true);
await archive.SetArchiveMetadataAsync(metadata);
- await archive.SetSourceSchemaAsync(schemaName, "schema"u8.ToArray());
+ await archive.SetSourceSchemaConfigurationAsync(schemaName, "schema"u8.ToArray(), CreateSettingsJson());
}
[Theory]
@@ -635,7 +631,10 @@ public async Task SetSourceSchema_WithInvalidSchemaNames_ThrowsException(string
await archive.SetArchiveMetadataAsync(metadata);
await Assert.ThrowsAsync(
- () => archive.SetSourceSchemaAsync(schemaName, "schema"u8.ToArray()));
+ () => archive.SetSourceSchemaConfigurationAsync(
+ schemaName,
+ "schema"u8.ToArray(),
+ CreateSettingsJson()));
}
[Fact]
@@ -678,6 +677,11 @@ private ArchiveMetadata CreateTestMetadata()
};
}
+ private JsonDocument CreateSettingsJson()
+ {
+ return JsonDocument.Parse("{ }");
+ }
+
private X509Certificate2 CreateTestCertificate()
{
using var rsa = RSA.Create(2048);