Adopted further C# 14 and .NET 10 features (#1158)

This commit is contained in:
Thorsten Sommer authored and GitHub committed 2026-11-04 14:04:10 +01:00
1 parent f649febf88
commit f0fa1411c8
40 files changed
+545 -56

No files matched your search

@@ -70,7 +70,7 @@ public static class ChatArchiveImporter
{
try
{
using var archive = ZipFile.OpenRead(archivePath);
await using var archive = await ZipFile.OpenReadAsync(archivePath, token);
var manifestResult = await DataBackupReader.ReadManifestAsync(archive, token);
if (manifestResult.Manifest is not { } manifest)
return new(false, new(), manifestResult.Issue);
@@ -114,7 +114,7 @@ public static class ChatArchiveImporter
try
{
var manifest = preview.Manifest;
using var archive = ZipFile.OpenRead(archivePath);
await using var archive = await ZipFile.OpenReadAsync(archivePath, token);
var entriesByChat = GroupEntriesByChat(archive);
var provenance = await ExtractProvenanceStoreAsync(archive, provenanceStore, provenanceDirectory, token);
@@ -243,7 +243,7 @@ public static class ChatArchiveImporter
/// <param name="token">The cancellation token.</param>
private static async Task ExtractEntryAsync(ZipArchiveEntry entry, string destinationPath, CancellationToken token)
{
await using var entryStream = entry.Open();
await using var entryStream = await entry.OpenAsync(token);
await using var fileStream = new FileStream(destinationPath, FileMode.Create, FileAccess.Write, FileShare.None, BUFFER_SIZE, FileOptions.Asynchronous | FileOptions.SequentialScan);
await entryStream.CopyToAsync(fileStream, token);
}
@@ -446,7 +446,7 @@ public static class ChatArchiveImporter
private static async Task<string> ReadTextAsync(ZipArchiveEntry entry, CancellationToken token)
{
using var reader = new StreamReader(entry.Open());
using var reader = new StreamReader(await entry.OpenAsync(token));
return (await reader.ReadToEndAsync(token)).Trim();
}
@@ -550,7 +550,7 @@ public static class ChatArchiveImporter
}
ChatThread? thread;
await using (var threadStream = threadEntry.Entry.Open())
await using (var threadStream = await threadEntry.Entry.OpenAsync(token))
thread = await JsonSerializer.DeserializeAsync<ChatThread>(threadStream, WorkspaceBehaviour.JSON_OPTIONS, token);
if (thread is null)
@@ -31,7 +31,7 @@ public static class DataBackupReader
DataBackupManifest? manifest;
try
{
await using var manifestStream = manifestEntry.Open();
await using var manifestStream = await manifestEntry.OpenAsync(token);
manifest = await JsonSerializer.DeserializeAsync<DataBackupManifest>(manifestStream, DataBackupFormat.JSON_OPTIONS, token);
}
catch (JsonException)
@@ -50,7 +50,7 @@ public static class DataBackupWriter
var buffer = new byte[BUFFER_SIZE];
progress?.Report(new(DataBackupPhase.WRITING, 0, totalFiles));
using (var archive = new ZipArchive(target, ZipArchiveMode.Create, leaveOpen: true))
await using (var archive = await ZipArchive.CreateAsync(target, ZipArchiveMode.Create, leaveOpen: true, entryNameEncoding: null, token))
{
var settingsEntry = DataBackupContents.ENTRIES.First(entry => entry.Kind is DataBackupEntryKind.SETTINGS);
await using (var settingsStream = new MemoryStream(Encoding.UTF8.GetBytes(sources.SettingsJson), writable: false))
@@ -179,7 +179,7 @@ public static class DataBackupWriter
using var hash = IncrementalHash.CreateHash(HashAlgorithmName.SHA256);
long size = 0;
await using (var entryStream = entry.Open())
await using (var entryStream = await entry.OpenAsync(token))
{
int read;
while ((read = await source.ReadAsync(buffer, token)) > 0)
@@ -47,7 +47,7 @@ public static class DataRestorePreparer
Discard(target);
try
{
using var archive = ZipFile.OpenRead(archivePath);
await using var archive = await ZipFile.OpenReadAsync(archivePath, token);
var manifestResult = await DataBackupReader.ReadManifestAsync(archive, token);
if (manifestResult.Manifest is not { } manifest)
return new(false, false, manifestResult.Issue);
@@ -218,7 +218,7 @@ public static class DataRestorePreparer
using var hash = IncrementalHash.CreateHash(HashAlgorithmName.SHA256);
long size = 0;
await using (var source = file.Entry.Open())
await using (var source = await file.Entry.OpenAsync(token))
await using (var destination = new FileStream(file.DestinationPath, FileMode.CreateNew, FileAccess.Write, FileShare.None, BUFFER_SIZE, FileOptions.Asynchronous | FileOptions.SequentialScan))
{
int read;
@@ -8,7 +8,15 @@ namespace AIStudio.Tools.ERIClient;
public abstract class ERIClientBase(IERIDataSource dataSource) : IDisposable
{
protected readonly IERIDataSource DataSource = dataSource;
/// <summary>
/// How AI Studio writes its requests to an ERI server and reads the answers.
/// </summary>
/// <remarks>
/// An answer which names a field twice is refused, also when the two names differ only in case:
/// AI Studio would follow one of the values, such as the provider types a data source allows, while
/// the server may have meant the other.
/// </remarks>
internal static readonly JsonSerializerOptions JSON_OPTIONS = new JsonSerializerOptions
{
WriteIndented = true,
@@ -17,6 +25,7 @@ public abstract class ERIClientBase(IERIDataSource dataSource) : IDisposable
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
DictionaryKeyPolicy = JsonNamingPolicy.CamelCase,
UnmappedMemberHandling = JsonUnmappedMemberHandling.Disallow,
AllowDuplicateProperties = false,
Converters =
{
new JsonStringEnumConverter(JsonNamingPolicy.SnakeCaseUpper),
@@ -21,8 +21,6 @@ public readonly record struct FileExtractionResult(FileExtractionOutcome Outcome
private static readonly int[] NO_FAILED_PAGES = [];
private static readonly PromptInjectionFinding[] NO_FINDINGS = [];
private readonly IReadOnlyList<PromptInjectionFinding>? promptInjectionFindings;
/// <summary>
/// The prompt-injection attempts the runtime filtered out of the content, if any.
/// </summary>
@@ -33,8 +31,8 @@ public readonly record struct FileExtractionResult(FileExtractionOutcome Outcome
/// </remarks>
public IReadOnlyList<PromptInjectionFinding> PromptInjectionFindings
{
get => this.promptInjectionFindings ?? NO_FINDINGS;
init => this.promptInjectionFindings = value;
get => field ?? NO_FINDINGS;
init;
}
/// <summary>
+1 -5
View File
@@ -196,11 +196,7 @@ public static partial class Pandoc
if (uri.EndsWith(".zip", StringComparison.OrdinalIgnoreCase))
{
await RunWithRetriesAsync(
() =>
{
ZipFile.ExtractToDirectory(pandocTempDownloadFile, stagingDir, true);
return Task.CompletedTask;
},
() => ZipFile.ExtractToDirectoryAsync(pandocTempDownloadFile, stagingDir, true),
"extracting the Pandoc ZIP archive");
}
else if (uri.EndsWith(".tar.gz", StringComparison.OrdinalIgnoreCase))
@@ -17,9 +17,9 @@ public static class PluginArchive
// Compatibility shim for Windows-created ZIPs with backslashes in entry names (dotnet/runtime#27620);
// remove after dotnet/runtime#27620 and #41914 are fixed.
// See documentation/compatibility-shims/2026-07-plugin-archive-zip-backslashes.md.
public static void Extract(string sourceArchiveFileName, string destinationDirectory)
public static async Task ExtractAsync(string sourceArchiveFileName, string destinationDirectory, CancellationToken token = default)
{
using var archive = ZipFile.OpenRead(sourceArchiveFileName);
await using var archive = await ZipFile.OpenReadAsync(sourceArchiveFileName, token);
Directory.CreateDirectory(destinationDirectory);
foreach (var entry in archive.Entries)
@@ -37,7 +37,7 @@ public static class PluginArchive
}
Directory.CreateDirectory(Path.GetDirectoryName(destinationPath)!);
entry.ExtractToFile(destinationPath);
await entry.ExtractToFileAsync(destinationPath, token);
}
}
}
@@ -64,7 +64,7 @@ public static partial class PluginFactory
await response.Content.CopyToAsync(tempFileStream, cancellationToken);
}
ExtractConfigPluginArchive(tempDownloadFile, stagedDirectory);
await ExtractConfigPluginArchiveAsync(tempDownloadFile, stagedDirectory, cancellationToken);
var configDirectory = Path.Join(ENTERPRISE_CONFIGURATION_PLUGINS_ROOT, configPlugId.ToString());
if (Directory.Exists(configDirectory))
@@ -129,9 +129,9 @@ public static partial class PluginFactory
return wasSuccessful;
}
private static void ExtractConfigPluginArchive(string sourceArchiveFileName, string destinationDirectory)
private static async Task ExtractConfigPluginArchiveAsync(string sourceArchiveFileName, string destinationDirectory, CancellationToken token)
{
PluginArchive.Extract(sourceArchiveFileName, destinationDirectory);
await PluginArchive.ExtractAsync(sourceArchiveFileName, destinationDirectory, token);
if (!Directory.EnumerateFiles(destinationDirectory, "plugin.lua", SearchOption.AllDirectories).Any())
throw new InvalidDataException("The enterprise configuration plugin archive does not contain a plugin.lua file.");
@@ -20,7 +20,7 @@ public sealed class GlobalShortcutService : BackgroundService, IMessageBusReceiv
}
private readonly SemaphoreSlim registrationSemaphore = new(1, 1);
private readonly object runtimeStateLock = new();
private readonly Lock runtimeStateLock = new();
private readonly Dictionary<Shortcut, ShortcutState> lastSentStates = [];
private readonly Dictionary<Shortcut, string> lastNonEmptyShortcuts = [];
private readonly Dictionary<Shortcut, ShortcutRuntimeBinding> runtimeBindings = [];
@@ -12,7 +12,7 @@ internal sealed partial class FileSourceIndexer
private readonly ConcurrentDictionary<string, DataSourceWatcherRegistration> watchers = new(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<string, CancellationTokenSource> watcherDebounceTokens = new(StringComparer.OrdinalIgnoreCase);
private readonly object watcherDebounceLock = new();
private readonly Lock watcherDebounceLock = new();
/// <summary>
/// The data sources handed in last, by their id. A watcher which failed is created anew from here.
@@ -45,7 +45,7 @@ public sealed partial class PluginInstallService
if (!this.TryCreateStagingDirectory(PLUGIN_IMPORT_DIRECTORY_PREFIX, out stagingDirectory, out var stagingIssue))
return Error(stagingIssue);
PluginArchive.Extract(archivePath, stagingDirectory);
await PluginArchive.ExtractAsync(archivePath, stagingDirectory, token);
var pluginFiles = Directory.EnumerateFiles(stagingDirectory, PLUGIN_FILE_NAME, SearchOption.AllDirectories).ToArray();
if (pluginFiles.Length != 1)
@@ -140,7 +140,7 @@ public sealed class PluginShareService(NativeShareService nativeShareService, Ru
// one next to it, which is what deleting and recreating would come down to.
//
await using var archiveStream = File.Create(archivePath);
ZipFile.CreateFromDirectory(pluginRoot, archiveStream, CompressionLevel.Optimal, false);
await ZipFile.CreateFromDirectoryAsync(pluginRoot, archiveStream, CompressionLevel.Optimal, false, token);
}, token);
logger.LogInformation("Exported plugin '{PluginName}' ({PluginId}) to the archive '{ArchivePath}'.", plugin.Name, plugin.Id, archivePath);
@@ -177,10 +177,10 @@ public sealed class PluginShareService(NativeShareService nativeShareService, Ru
Directory.CreateDirectory(archiveDirectory);
this.CleanUpExpiredArchives(archiveDirectory);
await Task.Run(() =>
await Task.Run(async () =>
{
token.ThrowIfCancellationRequested();
ZipFile.CreateFromDirectory(pluginRoot, archivePath, CompressionLevel.Optimal, false);
await ZipFile.CreateFromDirectoryAsync(pluginRoot, archivePath, CompressionLevel.Optimal, false, token);
}, token);
token.ThrowIfCancellationRequested();
@@ -14,6 +14,22 @@ public sealed class ToolExecutor(ToolSettingsService toolSettingsService, ILogge
{
private const string INVALID_TOOL_CALL_ERROR = "The tool call was invalid.";
/// <summary>
/// How the arguments are read for running a tool: every name only once per object.
/// </summary>
/// <remarks>
/// JSON allows a name twice, and readers disagree on which of the values counts. A tool reads one
/// of them, while the check for addresses the model wrote into its call reads both, and a provider
/// which receives the call back may read the other. A call which names an argument twice therefore
/// does not run, and the message of the parser, which names the argument but quotes none of the
/// values, goes back to the model. The trace reads the arguments without this rule, so that the
/// user still sees what the model sent.
/// </remarks>
private static readonly JsonDocumentOptions EXECUTION_ARGUMENT_OPTIONS = new()
{
AllowDuplicateProperties = false,
};
public ToolCallOutcome CreateInvalidToolCallResult(string toolCallId, int order)
{
logger.LogWarning(
@@ -116,7 +132,7 @@ public sealed class ToolExecutor(ToolSettingsService toolSettingsService, ILogge
var implementation = runnableTool.Implementation;
try
{
using var document = JsonDocument.Parse(string.IsNullOrWhiteSpace(argumentsJson) ? "{}" : argumentsJson);
using var document = JsonDocument.Parse(string.IsNullOrWhiteSpace(argumentsJson) ? "{}" : argumentsJson, EXECUTION_ARGUMENT_OPTIONS);
var settingsManager = Program.SERVICE_PROVIDER.GetRequiredService<SettingsManager>();
//