From 14674c11fb993daf2ad506ed2b3b71e0aff55025 Mon Sep 17 00:00:00 2001 From: Tom Date: Mon, 29 Jun 2026 19:21:15 +0200 Subject: [PATCH 01/39] build(tui): scaffold SquidStd.Tui project on Terminal.Gui v2 and CommunityToolkit.Mvvm --- SquidStd.slnx | 1 + .../Internal/PlaceholderMarker.cs | 5 +++++ src/SquidStd.Tui/SquidStd.Tui.csproj | 20 +++++++++++++++++++ tests/SquidStd.Tests/SquidStd.Tests.csproj | 1 + 4 files changed, 27 insertions(+) create mode 100644 src/SquidStd.Tui/Internal/PlaceholderMarker.cs create mode 100644 src/SquidStd.Tui/SquidStd.Tui.csproj diff --git a/SquidStd.slnx b/SquidStd.slnx index bbcd9f9d..b9ab6e14 100644 --- a/SquidStd.slnx +++ b/SquidStd.slnx @@ -41,6 +41,7 @@ + diff --git a/src/SquidStd.Tui/Internal/PlaceholderMarker.cs b/src/SquidStd.Tui/Internal/PlaceholderMarker.cs new file mode 100644 index 00000000..4c193c43 --- /dev/null +++ b/src/SquidStd.Tui/Internal/PlaceholderMarker.cs @@ -0,0 +1,5 @@ +namespace SquidStd.Tui.Internal; + +internal static class PlaceholderMarker +{ +} diff --git a/src/SquidStd.Tui/SquidStd.Tui.csproj b/src/SquidStd.Tui/SquidStd.Tui.csproj new file mode 100644 index 00000000..8e30afb1 --- /dev/null +++ b/src/SquidStd.Tui/SquidStd.Tui.csproj @@ -0,0 +1,20 @@ + + + + net10.0 + enable + enable + true + + + + + + + + + + + + + diff --git a/tests/SquidStd.Tests/SquidStd.Tests.csproj b/tests/SquidStd.Tests/SquidStd.Tests.csproj index a2127844..70fb3401 100644 --- a/tests/SquidStd.Tests/SquidStd.Tests.csproj +++ b/tests/SquidStd.Tests/SquidStd.Tests.csproj @@ -74,6 +74,7 @@ + From c976e2353d8a7bf43349f100d070d59b48ac2a39 Mon Sep 17 00:00:00 2001 From: Tom Date: Mon, 29 Jun 2026 19:24:16 +0200 Subject: [PATCH 02/39] feat(tui): add PropertyPath member-expression helpers --- .../Internal/PlaceholderMarker.cs | 5 --- src/SquidStd.Tui/Internal/PropertyPath.cs | 32 +++++++++++++++ .../Tui/Internal/PropertyPathTests.cs | 41 +++++++++++++++++++ 3 files changed, 73 insertions(+), 5 deletions(-) delete mode 100644 src/SquidStd.Tui/Internal/PlaceholderMarker.cs create mode 100644 src/SquidStd.Tui/Internal/PropertyPath.cs create mode 100644 tests/SquidStd.Tests/Tui/Internal/PropertyPathTests.cs diff --git a/src/SquidStd.Tui/Internal/PlaceholderMarker.cs b/src/SquidStd.Tui/Internal/PlaceholderMarker.cs deleted file mode 100644 index 4c193c43..00000000 --- a/src/SquidStd.Tui/Internal/PlaceholderMarker.cs +++ /dev/null @@ -1,5 +0,0 @@ -namespace SquidStd.Tui.Internal; - -internal static class PlaceholderMarker -{ -} diff --git a/src/SquidStd.Tui/Internal/PropertyPath.cs b/src/SquidStd.Tui/Internal/PropertyPath.cs new file mode 100644 index 00000000..8510a725 --- /dev/null +++ b/src/SquidStd.Tui/Internal/PropertyPath.cs @@ -0,0 +1,32 @@ +using System.Linq.Expressions; + +namespace SquidStd.Tui.Internal; + +/// Extracts member name, getter and setter from a member-access expression. +internal static class PropertyPath +{ + public static string NameOf(Expression> expression) + { + if (expression.Body is MemberExpression member) + { + return member.Member.Name; + } + + throw new ArgumentException("Expression must be a direct property or field access.", nameof(expression)); + } + + public static Action Setter(Expression> expression) + { + if (expression.Body is not MemberExpression member) + { + throw new ArgumentException("Expression must be a direct property or field access.", nameof(expression)); + } + + var targetParam = Expression.Parameter(typeof(TSource), "target"); + var valueParam = Expression.Parameter(typeof(TValue), "value"); + var memberAccess = Expression.MakeMemberAccess(targetParam, member.Member); + var assign = Expression.Assign(memberAccess, valueParam); + + return Expression.Lambda>(assign, targetParam, valueParam).Compile(); + } +} diff --git a/tests/SquidStd.Tests/Tui/Internal/PropertyPathTests.cs b/tests/SquidStd.Tests/Tui/Internal/PropertyPathTests.cs new file mode 100644 index 00000000..d1411c99 --- /dev/null +++ b/tests/SquidStd.Tests/Tui/Internal/PropertyPathTests.cs @@ -0,0 +1,41 @@ +using SquidStd.Tui.Internal; + +namespace SquidStd.Tests.Tui.Internal; + +public class PropertyPathTests +{ + private sealed class Sample + { + public string Name { get; set; } = string.Empty; + public int Count; + } + + [Fact] + public void NameOf_Property_ReturnsName() + { + Assert.Equal("Name", PropertyPath.NameOf(s => s.Name)); + } + + [Fact] + public void NameOf_Field_ReturnsName() + { + Assert.Equal("Count", PropertyPath.NameOf(s => s.Count)); + } + + [Fact] + public void Setter_Property_WritesValue() + { + var setter = PropertyPath.Setter(s => s.Name); + var target = new Sample(); + + setter(target, "abc"); + + Assert.Equal("abc", target.Name); + } + + [Fact] + public void NameOf_NonMemberExpression_Throws() + { + Assert.Throws(() => PropertyPath.NameOf(s => s.Count + 1)); + } +} From 615e6cdb5ce420674e2215765a3b357685e53a4a Mon Sep 17 00:00:00 2001 From: Tom Date: Mon, 29 Jun 2026 19:27:00 +0200 Subject: [PATCH 03/39] feat(tui): add ReentryGuard for two-way binding --- src/SquidStd.Tui/Internal/ReentryGuard.cs | 34 +++++++++++++++++ .../Tui/Internal/ReentryGuardTests.cs | 37 +++++++++++++++++++ 2 files changed, 71 insertions(+) create mode 100644 src/SquidStd.Tui/Internal/ReentryGuard.cs create mode 100644 tests/SquidStd.Tests/Tui/Internal/ReentryGuardTests.cs diff --git a/src/SquidStd.Tui/Internal/ReentryGuard.cs b/src/SquidStd.Tui/Internal/ReentryGuard.cs new file mode 100644 index 00000000..1bf5f195 --- /dev/null +++ b/src/SquidStd.Tui/Internal/ReentryGuard.cs @@ -0,0 +1,34 @@ +namespace SquidStd.Tui.Internal; + +/// Single-threaded reentrancy flag used to stop two-way binding write-back loops. +internal sealed class ReentryGuard +{ + private bool _busy; + + public bool IsBusy + { + get { return _busy; } + } + + public IDisposable Enter() + { + _busy = true; + + return new Scope(this); + } + + private sealed class Scope : IDisposable + { + private readonly ReentryGuard _owner; + + public Scope(ReentryGuard owner) + { + _owner = owner; + } + + public void Dispose() + { + _owner._busy = false; + } + } +} diff --git a/tests/SquidStd.Tests/Tui/Internal/ReentryGuardTests.cs b/tests/SquidStd.Tests/Tui/Internal/ReentryGuardTests.cs new file mode 100644 index 00000000..4b5b0259 --- /dev/null +++ b/tests/SquidStd.Tests/Tui/Internal/ReentryGuardTests.cs @@ -0,0 +1,37 @@ +using SquidStd.Tui.Internal; + +namespace SquidStd.Tests.Tui.Internal; + +public class ReentryGuardTests +{ + [Fact] + public void IsBusy_FalseByDefault_TrueInsideScope_FalseAfter() + { + var guard = new ReentryGuard(); + Assert.False(guard.IsBusy); + + using (guard.Enter()) + { + Assert.True(guard.IsBusy); + } + + Assert.False(guard.IsBusy); + } + + [Fact] + public void TryEnter_BlocksNestedEntry() + { + var guard = new ReentryGuard(); + var inner = 0; + + using (guard.Enter()) + { + if (!guard.IsBusy) + { + inner++; + } + } + + Assert.Equal(0, inner); + } +} From 65e45ad862f1c8f83942313a4e2514e45ac338a8 Mon Sep 17 00:00:00 2001 From: Tom Date: Mon, 29 Jun 2026 19:29:58 +0200 Subject: [PATCH 04/39] feat(tui): add ViewBinder one-way binding with marshal and disposal --- src/SquidStd.Tui/Binding/ViewBinder.cs | 55 ++++++++++++++++ src/SquidStd.Tui/Internal/Unsubscriber.cs | 19 ++++++ .../Tui/Binding/ViewBinderOneWayTests.cs | 66 +++++++++++++++++++ 3 files changed, 140 insertions(+) create mode 100644 src/SquidStd.Tui/Binding/ViewBinder.cs create mode 100644 src/SquidStd.Tui/Internal/Unsubscriber.cs create mode 100644 tests/SquidStd.Tests/Tui/Binding/ViewBinderOneWayTests.cs diff --git a/src/SquidStd.Tui/Binding/ViewBinder.cs b/src/SquidStd.Tui/Binding/ViewBinder.cs new file mode 100644 index 00000000..8bd17a89 --- /dev/null +++ b/src/SquidStd.Tui/Binding/ViewBinder.cs @@ -0,0 +1,55 @@ +using System.ComponentModel; +using SquidStd.Tui.Internal; + +namespace SquidStd.Tui.Binding; + +/// +/// Wires ViewModel () properties and commands to view targets, and +/// owns the lifetime of every subscription it creates. Dispose to release them all. +/// +public sealed partial class ViewBinder : IDisposable +{ + private readonly List _subscriptions = new(); + private readonly Action _marshal; + + /// + /// Runs an update on the UI thread. Defaults to running inline; the host passes a delegate over + /// Application.Invoke so updates raised from background threads reach the Terminal.Gui loop. + /// + public ViewBinder(Action? marshal = null) + { + _marshal = marshal ?? (action => action()); + } + + /// Applies now and whenever changes. + public void OneWay(INotifyPropertyChanged source, string propertyName, Action apply) + { + apply(); + + void Handler(object? sender, PropertyChangedEventArgs e) + { + if (e.PropertyName is null || string.Equals(e.PropertyName, propertyName, StringComparison.Ordinal)) + { + _marshal(apply); + } + } + + source.PropertyChanged += Handler; + _subscriptions.Add(new Unsubscriber(() => source.PropertyChanged -= Handler)); + } + + private void Track(IDisposable subscription) + { + _subscriptions.Add(subscription); + } + + public void Dispose() + { + for (var i = 0; i < _subscriptions.Count; i++) + { + _subscriptions[i].Dispose(); + } + + _subscriptions.Clear(); + } +} diff --git a/src/SquidStd.Tui/Internal/Unsubscriber.cs b/src/SquidStd.Tui/Internal/Unsubscriber.cs new file mode 100644 index 00000000..9c71e2f7 --- /dev/null +++ b/src/SquidStd.Tui/Internal/Unsubscriber.cs @@ -0,0 +1,19 @@ +namespace SquidStd.Tui.Internal; + +/// An that runs a single unsubscribe action once. +internal sealed class Unsubscriber : IDisposable +{ + private Action? _unsubscribe; + + public Unsubscriber(Action unsubscribe) + { + _unsubscribe = unsubscribe; + } + + public void Dispose() + { + var action = _unsubscribe; + _unsubscribe = null; + action?.Invoke(); + } +} diff --git a/tests/SquidStd.Tests/Tui/Binding/ViewBinderOneWayTests.cs b/tests/SquidStd.Tests/Tui/Binding/ViewBinderOneWayTests.cs new file mode 100644 index 00000000..9a831f62 --- /dev/null +++ b/tests/SquidStd.Tests/Tui/Binding/ViewBinderOneWayTests.cs @@ -0,0 +1,66 @@ +using System.ComponentModel; +using SquidStd.Tui.Binding; + +namespace SquidStd.Tests.Tui.Binding; + +public class ViewBinderOneWayTests +{ + private sealed class FakeViewModel : INotifyPropertyChanged + { + private string _title = string.Empty; + + public string Title + { + get { return _title; } + set + { + _title = value; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Title))); + } + } + + public event PropertyChangedEventHandler? PropertyChanged; + } + + [Fact] + public void OneWay_AppliesInitialValueAndUpdates() + { + var vm = new FakeViewModel { Title = "first" }; + var applied = string.Empty; + var binder = new ViewBinder(); + + binder.OneWay(vm, nameof(FakeViewModel.Title), () => applied = vm.Title); + + Assert.Equal("first", applied); + + vm.Title = "second"; + Assert.Equal("second", applied); + } + + [Fact] + public void Dispose_StopsUpdates() + { + var vm = new FakeViewModel(); + var applied = string.Empty; + var binder = new ViewBinder(); + binder.OneWay(vm, nameof(FakeViewModel.Title), () => applied = vm.Title); + + binder.Dispose(); + vm.Title = "after-dispose"; + + Assert.Equal(string.Empty, applied); + } + + [Fact] + public void OneWay_UsesMarshal() + { + var vm = new FakeViewModel(); + var marshalled = 0; + var binder = new ViewBinder(action => { marshalled++; action(); }); + binder.OneWay(vm, nameof(FakeViewModel.Title), () => { }); + + vm.Title = "x"; + + Assert.Equal(1, marshalled); // change marshalled once; the initial apply is NOT marshalled + } +} From 0620180a10e2aede1daf513d31c3735109e298fc Mon Sep 17 00:00:00 2001 From: Tom Date: Mon, 29 Jun 2026 19:34:38 +0200 Subject: [PATCH 05/39] feat(tui): add ViewBinder two-way binding and typed expression overloads --- .../Binding/ViewBinder.Expressions.cs | 49 +++++++++++ src/SquidStd.Tui/Binding/ViewBinder.cs | 55 ++++++++++++ .../Tui/Binding/ViewBinderTwoWayTests.cs | 87 +++++++++++++++++++ 3 files changed, 191 insertions(+) create mode 100644 src/SquidStd.Tui/Binding/ViewBinder.Expressions.cs create mode 100644 tests/SquidStd.Tests/Tui/Binding/ViewBinderTwoWayTests.cs diff --git a/src/SquidStd.Tui/Binding/ViewBinder.Expressions.cs b/src/SquidStd.Tui/Binding/ViewBinder.Expressions.cs new file mode 100644 index 00000000..c62354f9 --- /dev/null +++ b/src/SquidStd.Tui/Binding/ViewBinder.Expressions.cs @@ -0,0 +1,49 @@ +using System.ComponentModel; +using System.Linq.Expressions; +using SquidStd.Tui.Internal; + +namespace SquidStd.Tui.Binding; + +public sealed partial class ViewBinder +{ + /// One-way bind a source property to a target member, by expression. + public void OneWay( + TSource source, + Expression> sourceProperty, + TTarget target, + Expression> targetProperty + ) + where TSource : INotifyPropertyChanged + { + var name = PropertyPath.NameOf(sourceProperty); + var read = sourceProperty.Compile(); + var write = PropertyPath.Setter(targetProperty); + + OneWay(source, name, () => write(target, read(source))); + } + + /// Two-way bind a source property to a target member, by expression, given the target's change event. + public void TwoWay( + TSource source, + Expression> sourceProperty, + TTarget target, + Expression> targetProperty, + Action subscribeTargetChanged + ) + where TSource : INotifyPropertyChanged + { + var name = PropertyPath.NameOf(sourceProperty); + var read = sourceProperty.Compile(); + var readTarget = targetProperty.Compile(); + var write = PropertyPath.Setter(targetProperty); + var writeSource = PropertyPath.Setter(sourceProperty); + + TwoWay( + source, + name, + () => write(target, read(source)), + subscribeTargetChanged, + () => writeSource(source, readTarget(target)) + ); + } +} diff --git a/src/SquidStd.Tui/Binding/ViewBinder.cs b/src/SquidStd.Tui/Binding/ViewBinder.cs index 8bd17a89..3f89480c 100644 --- a/src/SquidStd.Tui/Binding/ViewBinder.cs +++ b/src/SquidStd.Tui/Binding/ViewBinder.cs @@ -38,6 +38,61 @@ void Handler(object? sender, PropertyChangedEventArgs e) _subscriptions.Add(new Unsubscriber(() => source.PropertyChanged -= Handler)); } + /// + /// Binds a source property both ways. runs on source changes; + /// runs when the target raises a change. A reentrancy guard stops + /// the source→target→source feedback loop. + /// + public void TwoWay( + INotifyPropertyChanged source, + string propertyName, + Action applyToTarget, + Action subscribeTargetChanged, + Action writeToSource + ) + { + var guard = new ReentryGuard(); + + applyToTarget(); + + void SourceHandler(object? sender, PropertyChangedEventArgs e) + { + if (e.PropertyName is not null && !string.Equals(e.PropertyName, propertyName, StringComparison.Ordinal)) + { + return; + } + + if (guard.IsBusy) + { + return; + } + + _marshal(() => + { + using (guard.Enter()) + { + applyToTarget(); + } + }); + } + + source.PropertyChanged += SourceHandler; + _subscriptions.Add(new Unsubscriber(() => source.PropertyChanged -= SourceHandler)); + + subscribeTargetChanged(() => + { + if (guard.IsBusy) + { + return; + } + + using (guard.Enter()) + { + writeToSource(); + } + }); + } + private void Track(IDisposable subscription) { _subscriptions.Add(subscription); diff --git a/tests/SquidStd.Tests/Tui/Binding/ViewBinderTwoWayTests.cs b/tests/SquidStd.Tests/Tui/Binding/ViewBinderTwoWayTests.cs new file mode 100644 index 00000000..586cff1d --- /dev/null +++ b/tests/SquidStd.Tests/Tui/Binding/ViewBinderTwoWayTests.cs @@ -0,0 +1,87 @@ +using System.ComponentModel; +using SquidStd.Tui.Binding; + +namespace SquidStd.Tests.Tui.Binding; + +public class ViewBinderTwoWayTests +{ + private sealed class FakeViewModel : INotifyPropertyChanged + { + private string _name = string.Empty; + + public string Name + { + get { return _name; } + set + { + if (string.Equals(_name, value, StringComparison.Ordinal)) + { + return; + } + + _name = value; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Name))); + } + } + + public event PropertyChangedEventHandler? PropertyChanged; + } + + private sealed class FakeField + { + private string _text = string.Empty; + + public string Text + { + get { return _text; } + set { _text = value; } + } + + public event Action? Changed; + + public void UserTypes(string value) + { + _text = value; + Changed?.Invoke(); + } + } + + [Fact] + public void TwoWay_PropagatesBothDirections_WithoutLooping() + { + var vm = new FakeViewModel { Name = "init" }; + var field = new FakeField(); + var binder = new ViewBinder(); + + binder.TwoWay( + vm, nameof(FakeViewModel.Name), + applyToTarget: () => field.Text = vm.Name, + subscribeTargetChanged: cb => field.Changed += () => cb(), + writeToSource: () => vm.Name = field.Text + ); + + Assert.Equal("init", field.Text); + + vm.Name = "from-vm"; + Assert.Equal("from-vm", field.Text); + + field.UserTypes("from-ui"); + Assert.Equal("from-ui", vm.Name); + } + + [Fact] + public void TwoWay_Typed_BindsViaExpressions() + { + var vm = new FakeViewModel { Name = "a" }; + var field = new FakeField(); + var binder = new ViewBinder(); + + binder.TwoWay(vm, x => x.Name, field, f => f.Text, cb => field.Changed += () => cb()); + + vm.Name = "b"; + Assert.Equal("b", field.Text); + + field.UserTypes("c"); + Assert.Equal("c", vm.Name); + } +} From 37bf874109f136b5a521a42b3d40c0356540ec42 Mon Sep 17 00:00:00 2001 From: Tom Date: Mon, 29 Jun 2026 19:40:22 +0200 Subject: [PATCH 06/39] feat(tui): add ViewBinder command binding with CanExecute tracking --- src/SquidStd.Tui/Binding/ViewBinder.cs | 26 +++++++++++ .../Tui/Binding/ViewBinderCommandTests.cs | 44 +++++++++++++++++++ 2 files changed, 70 insertions(+) create mode 100644 tests/SquidStd.Tests/Tui/Binding/ViewBinderCommandTests.cs diff --git a/src/SquidStd.Tui/Binding/ViewBinder.cs b/src/SquidStd.Tui/Binding/ViewBinder.cs index 3f89480c..345cf9fe 100644 --- a/src/SquidStd.Tui/Binding/ViewBinder.cs +++ b/src/SquidStd.Tui/Binding/ViewBinder.cs @@ -1,4 +1,5 @@ using System.ComponentModel; +using System.Windows.Input; using SquidStd.Tui.Internal; namespace SquidStd.Tui.Binding; @@ -93,6 +94,31 @@ void SourceHandler(object? sender, PropertyChangedEventArgs e) }); } + /// + /// Binds a command to a control: activates the command (when it + /// can execute), and tracks . + /// + public void Command(ICommand command, Action setEnabled, Action subscribeTrigger) + { + setEnabled(command.CanExecute(null)); + + void CanHandler(object? sender, EventArgs e) + { + _marshal(() => setEnabled(command.CanExecute(null))); + } + + command.CanExecuteChanged += CanHandler; + _subscriptions.Add(new Unsubscriber(() => command.CanExecuteChanged -= CanHandler)); + + subscribeTrigger(() => + { + if (command.CanExecute(null)) + { + command.Execute(null); + } + }); + } + private void Track(IDisposable subscription) { _subscriptions.Add(subscription); diff --git a/tests/SquidStd.Tests/Tui/Binding/ViewBinderCommandTests.cs b/tests/SquidStd.Tests/Tui/Binding/ViewBinderCommandTests.cs new file mode 100644 index 00000000..14101e56 --- /dev/null +++ b/tests/SquidStd.Tests/Tui/Binding/ViewBinderCommandTests.cs @@ -0,0 +1,44 @@ +using CommunityToolkit.Mvvm.Input; +using SquidStd.Tui.Binding; + +namespace SquidStd.Tests.Tui.Binding; + +public class ViewBinderCommandTests +{ + [Fact] + public void Command_ExecutesTrigger_AndReflectsCanExecute() + { + var executed = 0; + var canRun = false; + // ReSharper disable once AccessToModifiedClosure + var command = new RelayCommand(() => executed++, () => canRun); + var enabled = true; + Action? trigger = null; + var binder = new ViewBinder(); + + binder.Command(command, isEnabled => enabled = isEnabled, cb => trigger = cb); + + Assert.False(enabled); // initial CanExecute == false -> disabled + + canRun = true; + command.NotifyCanExecuteChanged(); + Assert.True(enabled); // CanExecuteChanged -> enabled + + trigger!(); // user activates the control + Assert.Equal(1, executed); + } + + [Fact] + public void Command_DoesNotExecuteWhenCanExecuteFalse() + { + var executed = 0; + var command = new RelayCommand(() => executed++, () => false); + Action? trigger = null; + var binder = new ViewBinder(); + binder.Command(command, _ => { }, cb => trigger = cb); + + trigger!(); + + Assert.Equal(0, executed); + } +} From 87bd8f9db09eaa3c5145f083300cfbbc54517806 Mon Sep 17 00:00:00 2001 From: Tom Date: Mon, 29 Jun 2026 19:44:39 +0200 Subject: [PATCH 07/39] feat(tui): add Terminal.Gui widget binder overloads (Label, Window, TextField, Button) --- .../Binding/ViewBinder.Widgets.cs | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 src/SquidStd.Tui/Binding/ViewBinder.Widgets.cs diff --git a/src/SquidStd.Tui/Binding/ViewBinder.Widgets.cs b/src/SquidStd.Tui/Binding/ViewBinder.Widgets.cs new file mode 100644 index 00000000..7755e43e --- /dev/null +++ b/src/SquidStd.Tui/Binding/ViewBinder.Widgets.cs @@ -0,0 +1,42 @@ +using System.ComponentModel; +using System.Linq.Expressions; +using System.Windows.Input; +using Terminal.Gui.Views; + +namespace SquidStd.Tui.Binding; + +/// Convenience overloads that wire the core binder to concrete Terminal.Gui widgets. +public sealed partial class ViewBinder +{ + /// One-way bind a source string property to a 's text. + public void OneWayText(TSource source, Expression> property, Label label) + where TSource : INotifyPropertyChanged + { + OneWay(source, property, label, l => l.Text); + } + + /// One-way bind a source string property to a 's title. + public void OneWayTitle(TSource source, Expression> property, Window window) + where TSource : INotifyPropertyChanged + { + OneWay(source, property, window, w => w.Title); + } + + /// Two-way bind a source string property to a . + public void TwoWay(TSource source, Expression> property, TextField field) + where TSource : INotifyPropertyChanged + { + // Terminal.Gui 2.4.16 exposes ValueChanged (EventHandler>) + // rather than a TextChanged event. + TwoWay(source, property, field, f => f.Text, callback => field.ValueChanged += (_, _) => callback()); + } + + /// Bind a command to a : Accepted triggers it, CanExecute drives Enabled. + public void Command(Button button, ICommand command) + { + // Use Accepted (post-accept, non-cancellable) rather than Accepting for this side-effect-only + // handler; Terminal.Gui 2.4.16 docs mark subscribing side effects to the cancellable Accepting + // phase as incorrect. + Command(command, enabled => button.Enabled = enabled, callback => button.Accepted += (_, _) => callback()); + } +} From 98528e325f143f3f35e71df86b597bb8dbe754cd Mon Sep 17 00:00:00 2001 From: Tom Date: Mon, 29 Jun 2026 19:52:48 +0200 Subject: [PATCH 08/39] feat(tui): add convention-based AutoBind over Terminal.Gui views MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add ConventionNames internal helper that strips known widget suffixes (Field, Label, Button, Text, View, Box, List) from a widget Id to derive the ViewModel property name, and appends "Command" for ICommand members - Add ViewBinder.AutoBind partial that iterates View.SubViews and convention-binds each named subview to the matching VM member: Button → ICommand property via Command(), TextField → string property two-way (OneWay + ValueChanged write-back), Label → string property one-way - Add ConventionNamesTests (5 cases: 4 MemberName theory + 1 CommandName) --- .../Binding/ViewBinder.AutoBind.cs | 81 +++++++++++++++++++ src/SquidStd.Tui/Internal/ConventionNames.cs | 25 ++++++ .../Tui/Internal/ConventionNamesTests.cs | 22 +++++ 3 files changed, 128 insertions(+) create mode 100644 src/SquidStd.Tui/Binding/ViewBinder.AutoBind.cs create mode 100644 src/SquidStd.Tui/Internal/ConventionNames.cs create mode 100644 tests/SquidStd.Tests/Tui/Internal/ConventionNamesTests.cs diff --git a/src/SquidStd.Tui/Binding/ViewBinder.AutoBind.cs b/src/SquidStd.Tui/Binding/ViewBinder.AutoBind.cs new file mode 100644 index 00000000..55c69aaa --- /dev/null +++ b/src/SquidStd.Tui/Binding/ViewBinder.AutoBind.cs @@ -0,0 +1,81 @@ +using System.ComponentModel; +using System.Reflection; +using System.Windows.Input; +using SquidStd.Tui.Internal; +using Terminal.Gui.ViewBase; +using Terminal.Gui.Views; + +namespace SquidStd.Tui.Binding; + +public sealed partial class ViewBinder +{ + /// + /// Convention binding: for each named subview, binds it to a matching ViewModel member by name + /// (e.g. NameFieldName, SaveButtonSaveCommand). Explicit bindings + /// declared before AutoBind win; AutoBind only fills the gaps it recognises (Label, TextField, Button). + /// + public void AutoBind(View view, INotifyPropertyChanged viewModel) + { + var vmType = viewModel.GetType(); + + foreach (var subview in view.SubViews) + { + var id = subview.Id; + + if (string.IsNullOrEmpty(id)) + { + continue; + } + + switch (subview) + { + case Button button: + BindCommandByName(vmType, viewModel, ConventionNames.CommandName(id), button); + break; + case TextField field: + BindStringByName(vmType, viewModel, ConventionNames.MemberName(id), field, twoWay: true, null); + break; + case Label label: + BindStringByName(vmType, viewModel, ConventionNames.MemberName(id), null, twoWay: false, label); + break; + } + } + } + + private void BindCommandByName(Type vmType, INotifyPropertyChanged vm, string memberName, Button button) + { + var property = vmType.GetProperty(memberName, BindingFlags.Public | BindingFlags.Instance); + + if (property is not null && property.GetValue(vm) is ICommand command) + { + Command(button, command); + } + } + + private void BindStringByName( + Type vmType, INotifyPropertyChanged vm, string memberName, TextField? field, bool twoWay, Label? label + ) + { + var property = vmType.GetProperty(memberName, BindingFlags.Public | BindingFlags.Instance); + + if (property is null || property.PropertyType != typeof(string)) + { + return; + } + + if (twoWay && field is not null) + { + TwoWay( + vm, + memberName, + () => field.Text = (string)(property.GetValue(vm) ?? string.Empty), + callback => field.ValueChanged += (_, _) => callback(), + () => property.SetValue(vm, field.Text) + ); + } + else if (label is not null) + { + OneWay(vm, memberName, () => label.Text = (string)(property.GetValue(vm) ?? string.Empty)); + } + } +} diff --git a/src/SquidStd.Tui/Internal/ConventionNames.cs b/src/SquidStd.Tui/Internal/ConventionNames.cs new file mode 100644 index 00000000..828200f2 --- /dev/null +++ b/src/SquidStd.Tui/Internal/ConventionNames.cs @@ -0,0 +1,25 @@ +namespace SquidStd.Tui.Internal; + +/// Maps a widget id to candidate ViewModel member names for convention binding. +internal static class ConventionNames +{ + private static readonly string[] _suffixes = ["Field", "Label", "Button", "Text", "View", "Box", "List"]; + + public static string MemberName(string widgetId) + { + foreach (var suffix in _suffixes) + { + if (widgetId.Length > suffix.Length && widgetId.EndsWith(suffix, StringComparison.Ordinal)) + { + return widgetId[..^suffix.Length]; + } + } + + return widgetId; + } + + public static string CommandName(string widgetId) + { + return MemberName(widgetId) + "Command"; + } +} diff --git a/tests/SquidStd.Tests/Tui/Internal/ConventionNamesTests.cs b/tests/SquidStd.Tests/Tui/Internal/ConventionNamesTests.cs new file mode 100644 index 00000000..3d23f76d --- /dev/null +++ b/tests/SquidStd.Tests/Tui/Internal/ConventionNamesTests.cs @@ -0,0 +1,22 @@ +using SquidStd.Tui.Internal; + +namespace SquidStd.Tests.Tui.Internal; + +public class ConventionNamesTests +{ + [Theory] + [InlineData("NameField", "Name")] + [InlineData("TitleLabel", "Title")] + [InlineData("SaveButton", "Save")] + [InlineData("Name", "Name")] + public void MemberName_StripsKnownWidgetSuffixes(string widgetId, string expected) + { + Assert.Equal(expected, ConventionNames.MemberName(widgetId)); + } + + [Fact] + public void CommandName_AppendsCommandSuffix() + { + Assert.Equal("SaveCommand", ConventionNames.CommandName("SaveButton")); + } +} From 8c3ed1a1d57a00cffe1d4a627f7c0be6c1eee65b Mon Sep 17 00:00:00 2001 From: Tom Date: Mon, 29 Jun 2026 19:58:08 +0200 Subject: [PATCH 09/39] feat(tui): add TuiViewModel base with navigation access and lifecycle hooks --- src/SquidStd.Tui/Interfaces/ITuiNavigator.cs | 17 ++++++ src/SquidStd.Tui/TuiViewModel.cs | 27 ++++++++++ tests/SquidStd.Tests/Tui/TuiViewModelTests.cs | 52 +++++++++++++++++++ 3 files changed, 96 insertions(+) create mode 100644 src/SquidStd.Tui/Interfaces/ITuiNavigator.cs create mode 100644 src/SquidStd.Tui/TuiViewModel.cs create mode 100644 tests/SquidStd.Tests/Tui/TuiViewModelTests.cs diff --git a/src/SquidStd.Tui/Interfaces/ITuiNavigator.cs b/src/SquidStd.Tui/Interfaces/ITuiNavigator.cs new file mode 100644 index 00000000..1452ed8f --- /dev/null +++ b/src/SquidStd.Tui/Interfaces/ITuiNavigator.cs @@ -0,0 +1,17 @@ +using SquidStd.Tui; + +namespace SquidStd.Tui.Interfaces; + +/// ViewModel-first navigation over a stack of screens. +public interface ITuiNavigator +{ + /// Number of screens currently on the navigation stack. + int Depth { get; } + + /// Resolves and its view and pushes it onto the stack. + Task NavigateToAsync(CancellationToken cancellationToken = default) + where TViewModel : TuiViewModel; + + /// Pops the current screen and reactivates the one beneath it. + Task BackAsync(CancellationToken cancellationToken = default); +} diff --git a/src/SquidStd.Tui/TuiViewModel.cs b/src/SquidStd.Tui/TuiViewModel.cs new file mode 100644 index 00000000..6566658b --- /dev/null +++ b/src/SquidStd.Tui/TuiViewModel.cs @@ -0,0 +1,27 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using SquidStd.Tui.Interfaces; + +namespace SquidStd.Tui; + +/// +/// Base class for TUI ViewModels. Extends (so CommunityToolkit's +/// [ObservableProperty]/[RelayCommand] generators apply) and adds navigation access plus +/// activation lifecycle hooks the navigator invokes. +/// +public abstract class TuiViewModel : ObservableObject +{ + /// The navigator, assigned by the navigation system when the screen is pushed. + public ITuiNavigator Navigator { get; internal set; } = null!; + + /// Invoked after the screen becomes active. Default is a no-op. + public virtual ValueTask OnActivatedAsync() + { + return ValueTask.CompletedTask; + } + + /// Invoked after the screen is removed or hidden. Default is a no-op. + public virtual ValueTask OnDeactivatedAsync() + { + return ValueTask.CompletedTask; + } +} diff --git a/tests/SquidStd.Tests/Tui/TuiViewModelTests.cs b/tests/SquidStd.Tests/Tui/TuiViewModelTests.cs new file mode 100644 index 00000000..6cd048e3 --- /dev/null +++ b/tests/SquidStd.Tests/Tui/TuiViewModelTests.cs @@ -0,0 +1,52 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using SquidStd.Tui; + +namespace SquidStd.Tests.Tui; + +public partial class TuiViewModelTests +{ + private sealed partial class SampleViewModel : TuiViewModel + { + [ObservableProperty] + private string _title = string.Empty; + + public int Activated { get; private set; } + + public override ValueTask OnActivatedAsync() + { + Activated++; + + return ValueTask.CompletedTask; + } + } + + [Fact] + public void ObservableProperty_RaisesPropertyChanged() + { + var vm = new SampleViewModel(); + var raised = new List(); + vm.PropertyChanged += (_, e) => raised.Add(e.PropertyName); + + vm.Title = "hello"; + + Assert.Contains(nameof(SampleViewModel.Title), raised); + } + + [Fact] + public async Task OnActivatedAsync_IsOverridable() + { + var vm = new SampleViewModel(); + + await vm.OnActivatedAsync(); + + Assert.Equal(1, vm.Activated); + } + + [Fact] + public async Task OnDeactivatedAsync_DefaultsToNoOp() + { + var vm = new SampleViewModel(); + + await vm.OnDeactivatedAsync(); // does not throw + } +} From a5b28be490ed962cf95e405bda26d67716f0464d Mon Sep 17 00:00:00 2001 From: Tom Date: Mon, 29 Jun 2026 20:01:53 +0200 Subject: [PATCH 10/39] feat(tui): add ViewModel-first navigator with view registry and host abstraction --- src/SquidStd.Tui/Interfaces/ITuiView.cs | 11 ++ src/SquidStd.Tui/Interfaces/ITuiViewHost.cs | 11 ++ src/SquidStd.Tui/Internal/TuiViewRegistry.cs | 22 ++++ src/SquidStd.Tui/Navigation/TuiNavigator.cs | 70 +++++++++++ .../Tui/Navigation/TuiNavigatorTests.cs | 109 ++++++++++++++++++ 5 files changed, 223 insertions(+) create mode 100644 src/SquidStd.Tui/Interfaces/ITuiView.cs create mode 100644 src/SquidStd.Tui/Interfaces/ITuiViewHost.cs create mode 100644 src/SquidStd.Tui/Internal/TuiViewRegistry.cs create mode 100644 src/SquidStd.Tui/Navigation/TuiNavigator.cs create mode 100644 tests/SquidStd.Tests/Tui/Navigation/TuiNavigatorTests.cs diff --git a/src/SquidStd.Tui/Interfaces/ITuiView.cs b/src/SquidStd.Tui/Interfaces/ITuiView.cs new file mode 100644 index 00000000..928f2944 --- /dev/null +++ b/src/SquidStd.Tui/Interfaces/ITuiView.cs @@ -0,0 +1,11 @@ +namespace SquidStd.Tui.Interfaces; + +/// Non-generic handle the navigator uses to bind a ViewModel and initialise a view. +public interface ITuiView +{ + /// Assigns the ViewModel instance to the view. + void Bind(object viewModel); + + /// Builds the layout and declares bindings. Called once after . + void Initialize(); +} diff --git a/src/SquidStd.Tui/Interfaces/ITuiViewHost.cs b/src/SquidStd.Tui/Interfaces/ITuiViewHost.cs new file mode 100644 index 00000000..838d2b43 --- /dev/null +++ b/src/SquidStd.Tui/Interfaces/ITuiViewHost.cs @@ -0,0 +1,11 @@ +namespace SquidStd.Tui.Interfaces; + +/// Displays and removes views. Abstracts the Terminal.Gui application from the navigator. +public interface ITuiViewHost +{ + /// Shows a view as the current screen. + void Show(object view); + + /// Removes a previously shown view. + void Remove(object view); +} diff --git a/src/SquidStd.Tui/Internal/TuiViewRegistry.cs b/src/SquidStd.Tui/Internal/TuiViewRegistry.cs new file mode 100644 index 00000000..323ffd58 --- /dev/null +++ b/src/SquidStd.Tui/Internal/TuiViewRegistry.cs @@ -0,0 +1,22 @@ +namespace SquidStd.Tui.Internal; + +/// Maps a ViewModel type to the View type that renders it. +public sealed class TuiViewRegistry +{ + private readonly Dictionary _map = new(); + + public void Map(Type viewModelType, Type viewType) + { + _map[viewModelType] = viewType; + } + + public Type ViewTypeFor(Type viewModelType) + { + if (_map.TryGetValue(viewModelType, out var viewType)) + { + return viewType; + } + + throw new InvalidOperationException($"No view registered for ViewModel '{viewModelType.Name}'."); + } +} diff --git a/src/SquidStd.Tui/Navigation/TuiNavigator.cs b/src/SquidStd.Tui/Navigation/TuiNavigator.cs new file mode 100644 index 00000000..9808f6d0 --- /dev/null +++ b/src/SquidStd.Tui/Navigation/TuiNavigator.cs @@ -0,0 +1,70 @@ +using DryIoc; +using SquidStd.Tui.Interfaces; +using SquidStd.Tui.Internal; + +namespace SquidStd.Tui.Navigation; + +/// Stack-based ViewModel-first navigator: resolves the ViewModel and its view from the container. +public sealed class TuiNavigator : ITuiNavigator +{ + private readonly IResolver _resolver; + private readonly TuiViewRegistry _registry; + private readonly ITuiViewHost _viewHost; + private readonly Stack _stack = new(); + + public int Depth + { + get { return _stack.Count; } + } + + public TuiNavigator(IResolver resolver, TuiViewRegistry registry, ITuiViewHost viewHost) + { + _resolver = resolver; + _registry = registry; + _viewHost = viewHost; + } + + public async Task NavigateToAsync(CancellationToken cancellationToken = default) + where TViewModel : TuiViewModel + { + var viewModel = (TViewModel)_resolver.Resolve(typeof(TViewModel)); + viewModel.Navigator = this; + + var viewType = _registry.ViewTypeFor(typeof(TViewModel)); + var view = (ITuiView)_resolver.Resolve(viewType); + view.Bind(viewModel); + view.Initialize(); + + _stack.Push(new Screen(viewModel, view)); + _viewHost.Show(view); + + await viewModel.OnActivatedAsync(); + } + + public async Task BackAsync(CancellationToken cancellationToken = default) + { + if (_stack.Count <= 1) + { + return; + } + + var current = _stack.Pop(); + _viewHost.Remove(current.View); + await current.ViewModel.OnDeactivatedAsync(); + + var beneath = _stack.Peek(); + await beneath.ViewModel.OnActivatedAsync(); + } + + private sealed class Screen + { + public TuiViewModel ViewModel { get; } + public ITuiView View { get; } + + public Screen(TuiViewModel viewModel, ITuiView view) + { + ViewModel = viewModel; + View = view; + } + } +} diff --git a/tests/SquidStd.Tests/Tui/Navigation/TuiNavigatorTests.cs b/tests/SquidStd.Tests/Tui/Navigation/TuiNavigatorTests.cs new file mode 100644 index 00000000..00b47e07 --- /dev/null +++ b/tests/SquidStd.Tests/Tui/Navigation/TuiNavigatorTests.cs @@ -0,0 +1,109 @@ +using DryIoc; +using SquidStd.Tui; +using SquidStd.Tui.Interfaces; +using SquidStd.Tui.Internal; +using SquidStd.Tui.Navigation; + +namespace SquidStd.Tests.Tui.Navigation; + +public class TuiNavigatorTests +{ + private sealed class HomeViewModel : TuiViewModel + { + } + + private sealed class DetailViewModel : TuiViewModel + { + } + + // Non-generic fake view that records the ViewModel it was given and that it was initialised. + private sealed class FakeView : ITuiView + { + public object? BoundViewModel { get; private set; } + public bool Initialized { get; private set; } + + public void Bind(object viewModel) + { + BoundViewModel = viewModel; + } + + public void Initialize() + { + Initialized = true; + } + } + + private sealed class FakeViewHost : ITuiViewHost + { + public List Shown { get; } = new(); + public List Removed { get; } = new(); + + public void Show(object view) + { + Shown.Add(view); + } + + public void Remove(object view) + { + Removed.Add(view); + } + } + + private static (TuiNavigator Navigator, FakeViewHost Host) Build() + { + var container = new Container(); + container.Register(Reuse.Transient); + container.Register(Reuse.Transient); + container.Register(Reuse.Transient); + + var registry = new TuiViewRegistry(); + registry.Map(typeof(HomeViewModel), typeof(FakeView)); + registry.Map(typeof(DetailViewModel), typeof(FakeView)); + + var host = new FakeViewHost(); + var navigator = new TuiNavigator(container, registry, host); + + return (navigator, host); + } + + [Fact] + public async Task NavigateTo_PushesViewModelBoundView_AndShowsIt() + { + var (navigator, host) = Build(); + + await navigator.NavigateToAsync(); + + Assert.Equal(1, navigator.Depth); + Assert.Single(host.Shown); + var view = Assert.IsType(host.Shown[0]); + Assert.True(view.Initialized); + Assert.IsType(view.BoundViewModel); + Assert.Same(navigator, ((HomeViewModel)view.BoundViewModel!).Navigator); + } + + [Fact] + public async Task Back_PopsCurrentAndRemovesItsView() + { + var (navigator, host) = Build(); + await navigator.NavigateToAsync(); + await navigator.NavigateToAsync(); + + Assert.Equal(2, navigator.Depth); + + await navigator.BackAsync(); + + Assert.Equal(1, navigator.Depth); + Assert.Single(host.Removed); + } + + [Fact] + public async Task Back_OnLastScreen_DoesNothing() + { + var (navigator, _) = Build(); + await navigator.NavigateToAsync(); + + await navigator.BackAsync(); + + Assert.Equal(1, navigator.Depth); // refuses to pop the root + } +} From c7446cc3d8fa00772117f03a2a93a16954c8bf7c Mon Sep 17 00:00:00 2001 From: Tom Date: Mon, 29 Jun 2026 20:07:06 +0200 Subject: [PATCH 11/39] feat(tui): add TuiView base and Terminal.Gui view host --- .../Hosting/TerminalGuiViewHost.cs | 27 ++++++++++ src/SquidStd.Tui/TuiView.cs | 53 +++++++++++++++++++ 2 files changed, 80 insertions(+) create mode 100644 src/SquidStd.Tui/Hosting/TerminalGuiViewHost.cs create mode 100644 src/SquidStd.Tui/TuiView.cs diff --git a/src/SquidStd.Tui/Hosting/TerminalGuiViewHost.cs b/src/SquidStd.Tui/Hosting/TerminalGuiViewHost.cs new file mode 100644 index 00000000..c60ad9f6 --- /dev/null +++ b/src/SquidStd.Tui/Hosting/TerminalGuiViewHost.cs @@ -0,0 +1,27 @@ +using SquidStd.Tui.Interfaces; +using Terminal.Gui.App; +using Terminal.Gui.ViewBase; + +namespace SquidStd.Tui.Hosting; + +/// Shows views as top-level windows on the running Terminal.Gui application. +public sealed class TerminalGuiViewHost : ITuiViewHost +{ + public void Show(object view) + { + if (view is View concrete) + { + Application.TopRunnableView?.Add(concrete); + concrete.SetFocus(); + } + } + + public void Remove(object view) + { + if (view is View concrete) + { + Application.TopRunnableView?.Remove(concrete); + concrete.Dispose(); + } + } +} diff --git a/src/SquidStd.Tui/TuiView.cs b/src/SquidStd.Tui/TuiView.cs new file mode 100644 index 00000000..02926395 --- /dev/null +++ b/src/SquidStd.Tui/TuiView.cs @@ -0,0 +1,53 @@ +using SquidStd.Tui.Binding; +using SquidStd.Tui.Interfaces; +using Terminal.Gui.App; +using Terminal.Gui.Views; + +namespace SquidStd.Tui; + +/// +/// Base class for views: a Terminal.Gui that owns a typed ViewModel and a +/// . Implement to create widgets and +/// to declare bindings; both run once during . +/// +/// The ViewModel type for this view. +public abstract class TuiView : Window, ITuiView + where TViewModel : TuiViewModel +{ + private readonly ViewBinder _binder; + + /// The bound ViewModel. Set by the navigator before . + protected TViewModel ViewModel { get; private set; } = null!; + + protected TuiView() + { + _binder = new ViewBinder(Application.Invoke); + } + + void ITuiView.Bind(object viewModel) + { + ViewModel = (TViewModel)viewModel; + } + + void ITuiView.Initialize() + { + BuildLayout(); + Bind(_binder); + } + + /// Creates the Terminal.Gui widgets for this view. + protected abstract void BuildLayout(); + + /// Declares the bindings between and the widgets. + protected abstract void Bind(ViewBinder binder); + + protected override void Dispose(bool disposing) + { + if (disposing) + { + _binder.Dispose(); + } + + base.Dispose(disposing); + } +} From b37061fc0670be240666787c124c6ec578cc88fc Mon Sep 17 00:00:00 2001 From: Tom Date: Mon, 29 Jun 2026 20:21:39 +0200 Subject: [PATCH 12/39] feat(tui): add application host, DI registration extensions and README - Add TuiApplicationHost: boots Terminal.Gui via Application.Init/Run(shell)/Shutdown; creates a borderless full-screen Window shell before NavigateToAsync so the first screen is added to a non-null container (fixes the null Application.TopRunnableView race under Terminal.Gui 2.4.16 which removed Application.Top/Toplevel) - Revise TerminalGuiViewHost: replace Application.TopRunnableView usage with an explicit View? Container property set by TuiApplicationHost; Show/Remove operate on the guaranteed-non-null shell, sizing each child with Dim.Fill() - Add RegisterTuiExtensions (C# 14 extension block on IContainer): RegisterTui() wires TuiViewRegistry, TerminalGuiViewHost (singleton), ITuiViewHost mapped to the same concrete singleton via RegisterMapping<,>, ITuiNavigator, TuiApplicationHost; RegisterView() registers the pair transient and maps in the registry - Add DI tests RegisterTuiExtensionsTests (2 tests, TDD: written before production code) - Add README.md for SquidStd.Tui --- .../Extensions/RegisterTuiExtensions.cs | 38 +++++++++++ .../Hosting/TerminalGuiViewHost.cs | 20 ++++-- .../Hosting/TuiApplicationHost.cs | 51 +++++++++++++++ src/SquidStd.Tui/README.md | 64 +++++++++++++++++++ .../Extensions/RegisterTuiExtensionsTests.cs | 51 +++++++++++++++ 5 files changed, 218 insertions(+), 6 deletions(-) create mode 100644 src/SquidStd.Tui/Extensions/RegisterTuiExtensions.cs create mode 100644 src/SquidStd.Tui/Hosting/TuiApplicationHost.cs create mode 100644 src/SquidStd.Tui/README.md create mode 100644 tests/SquidStd.Tests/Tui/Extensions/RegisterTuiExtensionsTests.cs diff --git a/src/SquidStd.Tui/Extensions/RegisterTuiExtensions.cs b/src/SquidStd.Tui/Extensions/RegisterTuiExtensions.cs new file mode 100644 index 00000000..f7797450 --- /dev/null +++ b/src/SquidStd.Tui/Extensions/RegisterTuiExtensions.cs @@ -0,0 +1,38 @@ +using DryIoc; +using SquidStd.Tui.Hosting; +using SquidStd.Tui.Interfaces; +using SquidStd.Tui.Internal; +using SquidStd.Tui.Navigation; + +namespace SquidStd.Tui.Extensions; + +/// DryIoc registration helpers for the TUI module. +public static class RegisterTuiExtensions +{ + extension(IContainer container) + { + /// Registers the navigator, view registry, view host and application host. + public IContainer RegisterTui() + { + container.Register(Reuse.Singleton); + container.Register(Reuse.Singleton); + container.RegisterMapping(); + container.Register(Reuse.Singleton); + container.Register(Reuse.Singleton); + + return container; + } + + /// Registers a View/ViewModel pair and records the mapping for navigation. + public IContainer RegisterView() + where TView : class, ITuiView + where TViewModel : TuiViewModel + { + container.Register(Reuse.Transient); + container.Register(Reuse.Transient); + container.Resolve().Map(typeof(TViewModel), typeof(TView)); + + return container; + } + } +} diff --git a/src/SquidStd.Tui/Hosting/TerminalGuiViewHost.cs b/src/SquidStd.Tui/Hosting/TerminalGuiViewHost.cs index c60ad9f6..36cb07c3 100644 --- a/src/SquidStd.Tui/Hosting/TerminalGuiViewHost.cs +++ b/src/SquidStd.Tui/Hosting/TerminalGuiViewHost.cs @@ -1,26 +1,34 @@ using SquidStd.Tui.Interfaces; -using Terminal.Gui.App; using Terminal.Gui.ViewBase; namespace SquidStd.Tui.Hosting; -/// Shows views as top-level windows on the running Terminal.Gui application. +/// +/// Shows views as full-size children of a shell container supplied by . +/// The container is set before the first navigation, so never no-ops on a null top view +/// (Terminal.Gui 2.4.16 has no Application.Top; the running top view is null until the loop starts). +/// public sealed class TerminalGuiViewHost : ITuiViewHost { + /// The shell the navigator's screens are added to. Set by the application host before running. + public View? Container { get; set; } + public void Show(object view) { - if (view is View concrete) + if (Container is not null && view is View concrete) { - Application.TopRunnableView?.Add(concrete); + concrete.Width = Dim.Fill(); + concrete.Height = Dim.Fill(); + Container.Add(concrete); concrete.SetFocus(); } } public void Remove(object view) { - if (view is View concrete) + if (Container is not null && view is View concrete) { - Application.TopRunnableView?.Remove(concrete); + Container.Remove(concrete); concrete.Dispose(); } } diff --git a/src/SquidStd.Tui/Hosting/TuiApplicationHost.cs b/src/SquidStd.Tui/Hosting/TuiApplicationHost.cs new file mode 100644 index 00000000..265ae398 --- /dev/null +++ b/src/SquidStd.Tui/Hosting/TuiApplicationHost.cs @@ -0,0 +1,51 @@ +using SquidStd.Tui.Interfaces; +using Terminal.Gui.App; +using Terminal.Gui.Drawing; +using Terminal.Gui.ViewBase; +using Terminal.Gui.Views; + +namespace SquidStd.Tui.Hosting; + +/// +/// Boots a Terminal.Gui application: initialises the driver, creates a full-screen shell, navigates to the +/// root ViewModel (whose view is added to the shell), runs the event loop, then shuts the driver down. +/// +public sealed class TuiApplicationHost +{ + private readonly ITuiNavigator _navigator; + private readonly TerminalGuiViewHost _viewHost; + + public TuiApplicationHost(ITuiNavigator navigator, TerminalGuiViewHost viewHost) + { + _navigator = navigator; + _viewHost = viewHost; + } + + /// Runs the application with as the first screen. + public async Task RunAsync() + where TRootViewModel : TuiViewModel + { + Application.Init(); + + try + { + var shell = new Window + { + Width = Dim.Fill(), + Height = Dim.Fill(), + BorderStyle = LineStyle.None + }; + + _viewHost.Container = shell; + + await _navigator.NavigateToAsync(); + + Application.Run(shell); + } + finally + { + _viewHost.Container = null; + Application.Shutdown(); + } + } +} diff --git a/src/SquidStd.Tui/README.md b/src/SquidStd.Tui/README.md new file mode 100644 index 00000000..cda531ff --- /dev/null +++ b/src/SquidStd.Tui/README.md @@ -0,0 +1,64 @@ +

SquidStd.Tui

+ +MVVM for terminal apps, built on **Terminal.Gui v2** and **CommunityToolkit.Mvvm**. Observable +ViewModels, a View↔ViewModel binder (fluent + opt-in convention), ViewModel-first navigation, and DryIoc +wiring. + +## Install + +```bash +dotnet add package SquidStd.Tui +``` + +## Usage + +```csharp +public sealed partial class CounterViewModel : TuiViewModel +{ + [ObservableProperty] private string _title = "Counter"; + [ObservableProperty] private string _value = "0"; + + [RelayCommand] + private void Increment() => Value = (int.Parse(Value) + 1).ToString(); +} + +public sealed class CounterView : TuiView +{ + private Label _value = null!; + private Button _inc = null!; + + protected override void BuildLayout() + { + _value = new Label { X = 1, Y = 1 }; + _inc = new Button { X = 1, Y = 3, Text = "+1" }; + Add(_value, _inc); + } + + protected override void Bind(ViewBinder b) + { + b.OneWayTitle(ViewModel, x => x.Title, this); + b.OneWayText(ViewModel, x => x.Value, _value); + b.Command(_inc, ViewModel.IncrementCommand); + } +} + +// boot +var container = new Container().RegisterTui(); +container.RegisterView(); +await container.Resolve().RunAsync(); +``` + +## Key types + +| Type | Purpose | +|------|---------| +| `TuiViewModel` | ViewModel base (ObservableObject + activation hooks + `Navigator`). | +| `TuiView` | View base: a Terminal.Gui `Window` with a typed ViewModel and a binder. | +| `ViewBinder` | One-way / two-way / command binding; fluent typed + `AutoBind` by convention. | +| `ITuiNavigator` | ViewModel-first stack navigation (`NavigateToAsync`, `BackAsync`). | +| `TuiApplicationHost` | Boots the Terminal.Gui loop with a root ViewModel. | +| `RegisterTui()` / `RegisterView<,>()` | DryIoc registration. | + +## License + +MIT — part of [SquidStd](https://github.com/tgiachi/squid-std). diff --git a/tests/SquidStd.Tests/Tui/Extensions/RegisterTuiExtensionsTests.cs b/tests/SquidStd.Tests/Tui/Extensions/RegisterTuiExtensionsTests.cs new file mode 100644 index 00000000..71e8fd68 --- /dev/null +++ b/tests/SquidStd.Tests/Tui/Extensions/RegisterTuiExtensionsTests.cs @@ -0,0 +1,51 @@ +using DryIoc; +using SquidStd.Tui; +using SquidStd.Tui.Extensions; +using SquidStd.Tui.Interfaces; +using SquidStd.Tui.Internal; + +namespace SquidStd.Tests.Tui.Extensions; + +public class RegisterTuiExtensionsTests +{ + private sealed class HomeViewModel : TuiViewModel + { + } + + private sealed class HomeView : ITuiView + { + public void Bind(object viewModel) + { + } + + public void Initialize() + { + } + } + + [Fact] + public void RegisterTui_RegistersNavigatorAndRegistry() + { + var container = new Container(); + + container.RegisterTui(); + + Assert.NotNull(container.Resolve()); + Assert.NotNull(container.Resolve()); + Assert.NotNull(container.Resolve()); + } + + [Fact] + public void RegisterView_MapsAndResolvesBothTypes() + { + var container = new Container(); + container.RegisterTui(); + + container.RegisterView(); + + var registry = container.Resolve(); + Assert.Equal(typeof(HomeView), registry.ViewTypeFor(typeof(HomeViewModel))); + Assert.NotNull(container.Resolve()); + Assert.NotNull(container.Resolve()); + } +} From 9955edeee467e0426da977fd9177cc9ed36cd838 Mon Sep 17 00:00:00 2001 From: Tom Date: Mon, 29 Jun 2026 20:27:36 +0200 Subject: [PATCH 13/39] build(tui): add runnable Counter sample and host wiring smoke tests - add samples/SquidStd.Samples.Tui with CounterViewModel, CounterView, Program.cs - register SquidStd.Samples.Tui in SquidStd.slnx under /samples/ folder - add TerminalGuiViewHostTests: headless Show/Remove coverage for blank-screen regression - harden RegisterTui_RegistersNavigatorAndRegistry with Assert.Same singleton guard (TerminalGuiViewHost and ITuiViewHost must be the same instance) --- SquidStd.slnx | 1 + samples/SquidStd.Samples.Tui/CounterView.cs | 25 +++++++++++++++ .../SquidStd.Samples.Tui/CounterViewModel.cs | 20 ++++++++++++ samples/SquidStd.Samples.Tui/Program.cs | 10 ++++++ .../SquidStd.Samples.Tui.csproj | 14 ++++++++ .../Extensions/RegisterTuiExtensionsTests.cs | 2 ++ .../Tui/Hosting/TerminalGuiViewHostTests.cs | 32 +++++++++++++++++++ 7 files changed, 104 insertions(+) create mode 100644 samples/SquidStd.Samples.Tui/CounterView.cs create mode 100644 samples/SquidStd.Samples.Tui/CounterViewModel.cs create mode 100644 samples/SquidStd.Samples.Tui/Program.cs create mode 100644 samples/SquidStd.Samples.Tui/SquidStd.Samples.Tui.csproj create mode 100644 tests/SquidStd.Tests/Tui/Hosting/TerminalGuiViewHostTests.cs diff --git a/SquidStd.slnx b/SquidStd.slnx index b9ab6e14..b6e06793 100644 --- a/SquidStd.slnx +++ b/SquidStd.slnx @@ -64,6 +64,7 @@ + diff --git a/samples/SquidStd.Samples.Tui/CounterView.cs b/samples/SquidStd.Samples.Tui/CounterView.cs new file mode 100644 index 00000000..ec702153 --- /dev/null +++ b/samples/SquidStd.Samples.Tui/CounterView.cs @@ -0,0 +1,25 @@ +using SquidStd.Tui; +using SquidStd.Tui.Binding; +using Terminal.Gui.Views; + +namespace SquidStd.Samples.Tui; + +public sealed class CounterView : TuiView +{ + private Label _value = null!; + private Button _increment = null!; + + protected override void BuildLayout() + { + _value = new Label { X = 1, Y = 1 }; + _increment = new Button { X = 1, Y = 3, Text = "_Increment" }; + Add(_value, _increment); + } + + protected override void Bind(ViewBinder binder) + { + binder.OneWayTitle(ViewModel, x => x.Title, this); + binder.OneWayText(ViewModel, x => x.Value, _value); + binder.Command(_increment, ViewModel.IncrementCommand); + } +} diff --git a/samples/SquidStd.Samples.Tui/CounterViewModel.cs b/samples/SquidStd.Samples.Tui/CounterViewModel.cs new file mode 100644 index 00000000..7f41c638 --- /dev/null +++ b/samples/SquidStd.Samples.Tui/CounterViewModel.cs @@ -0,0 +1,20 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using SquidStd.Tui; + +namespace SquidStd.Samples.Tui; + +public sealed partial class CounterViewModel : TuiViewModel +{ + [ObservableProperty] + private string _title = "SquidStd.Tui — Counter"; + + [ObservableProperty] + private string _value = "0"; + + [RelayCommand] + private void Increment() + { + Value = (int.Parse(Value) + 1).ToString(); + } +} diff --git a/samples/SquidStd.Samples.Tui/Program.cs b/samples/SquidStd.Samples.Tui/Program.cs new file mode 100644 index 00000000..06339c6f --- /dev/null +++ b/samples/SquidStd.Samples.Tui/Program.cs @@ -0,0 +1,10 @@ +using DryIoc; +using SquidStd.Samples.Tui; +using SquidStd.Tui.Extensions; +using SquidStd.Tui.Hosting; + +var container = new Container(); +container.RegisterTui(); +container.RegisterView(); + +await container.Resolve().RunAsync(); diff --git a/samples/SquidStd.Samples.Tui/SquidStd.Samples.Tui.csproj b/samples/SquidStd.Samples.Tui/SquidStd.Samples.Tui.csproj new file mode 100644 index 00000000..40bc8813 --- /dev/null +++ b/samples/SquidStd.Samples.Tui/SquidStd.Samples.Tui.csproj @@ -0,0 +1,14 @@ + + + + Exe + net10.0 + enable + enable + + + + + + + diff --git a/tests/SquidStd.Tests/Tui/Extensions/RegisterTuiExtensionsTests.cs b/tests/SquidStd.Tests/Tui/Extensions/RegisterTuiExtensionsTests.cs index 71e8fd68..bc3e3cf5 100644 --- a/tests/SquidStd.Tests/Tui/Extensions/RegisterTuiExtensionsTests.cs +++ b/tests/SquidStd.Tests/Tui/Extensions/RegisterTuiExtensionsTests.cs @@ -1,6 +1,7 @@ using DryIoc; using SquidStd.Tui; using SquidStd.Tui.Extensions; +using SquidStd.Tui.Hosting; using SquidStd.Tui.Interfaces; using SquidStd.Tui.Internal; @@ -33,6 +34,7 @@ public void RegisterTui_RegistersNavigatorAndRegistry() Assert.NotNull(container.Resolve()); Assert.NotNull(container.Resolve()); Assert.NotNull(container.Resolve()); + Assert.Same(container.Resolve(), container.Resolve()); } [Fact] diff --git a/tests/SquidStd.Tests/Tui/Hosting/TerminalGuiViewHostTests.cs b/tests/SquidStd.Tests/Tui/Hosting/TerminalGuiViewHostTests.cs new file mode 100644 index 00000000..3b8a9952 --- /dev/null +++ b/tests/SquidStd.Tests/Tui/Hosting/TerminalGuiViewHostTests.cs @@ -0,0 +1,32 @@ +using SquidStd.Tui.Hosting; +using Terminal.Gui.ViewBase; + +namespace SquidStd.Tests.Tui.Hosting; + +public class TerminalGuiViewHostTests +{ + [Fact] + public void Show_AddsViewToContainer_AndRemoveDetachesIt() + { + var host = new TerminalGuiViewHost(); + var shell = new View(); + host.Container = shell; + + var screen = new View(); + host.Show(screen); + + Assert.Contains(screen, shell.SubViews); + + host.Remove(screen); + + Assert.DoesNotContain(screen, shell.SubViews); + } + + [Fact] + public void Show_WithNullContainer_DoesNotThrow() + { + var host = new TerminalGuiViewHost(); + + host.Show(new View()); // no container set -> no-op, must not throw + } +} From 3f6dbcb0f2c00a1d639b07daa9207240cc7ca050 Mon Sep 17 00:00:00 2001 From: Tom Date: Mon, 29 Jun 2026 20:48:07 +0200 Subject: [PATCH 14/39] refactor(tui): pair navigator activation hooks, cover AutoBind, drop dead code - TuiNavigator: deactivate the covered screen before pushing a new one so OnActivatedAsync/OnDeactivatedAsync are paired (no double-activate on forward-then-back navigation) - add ViewBinderAutoBindTests covering the reflection binding paths (label one-way, textfield two-way apply, button command discovery, unrecognised/wrong-type skip) - delete unused ViewBinder.Track method - add missing XML summaries on ViewBinder ctor/Dispose, TuiViewRegistry Map/ViewTypeFor and TerminalGuiViewHost Show/Remove --- src/SquidStd.Tui/Binding/ViewBinder.cs | 7 +- .../Hosting/TerminalGuiViewHost.cs | 2 + src/SquidStd.Tui/Internal/TuiViewRegistry.cs | 2 + src/SquidStd.Tui/Navigation/TuiNavigator.cs | 5 + .../Tui/Binding/ViewBinderAutoBindTests.cs | 164 ++++++++++++++++++ .../Tui/Navigation/TuiNavigatorTests.cs | 67 +++++++ 6 files changed, 242 insertions(+), 5 deletions(-) create mode 100644 tests/SquidStd.Tests/Tui/Binding/ViewBinderAutoBindTests.cs diff --git a/src/SquidStd.Tui/Binding/ViewBinder.cs b/src/SquidStd.Tui/Binding/ViewBinder.cs index 345cf9fe..82a9385a 100644 --- a/src/SquidStd.Tui/Binding/ViewBinder.cs +++ b/src/SquidStd.Tui/Binding/ViewBinder.cs @@ -13,6 +13,7 @@ public sealed partial class ViewBinder : IDisposable private readonly List _subscriptions = new(); private readonly Action _marshal; + /// Initialises the binder with an optional marshal action for UI-thread dispatch. /// /// Runs an update on the UI thread. Defaults to running inline; the host passes a delegate over /// Application.Invoke so updates raised from background threads reach the Terminal.Gui loop. @@ -119,11 +120,7 @@ void CanHandler(object? sender, EventArgs e) }); } - private void Track(IDisposable subscription) - { - _subscriptions.Add(subscription); - } - + /// Disposes all active subscriptions created by this binder. public void Dispose() { for (var i = 0; i < _subscriptions.Count; i++) diff --git a/src/SquidStd.Tui/Hosting/TerminalGuiViewHost.cs b/src/SquidStd.Tui/Hosting/TerminalGuiViewHost.cs index 36cb07c3..86dcd060 100644 --- a/src/SquidStd.Tui/Hosting/TerminalGuiViewHost.cs +++ b/src/SquidStd.Tui/Hosting/TerminalGuiViewHost.cs @@ -13,6 +13,7 @@ public sealed class TerminalGuiViewHost : ITuiViewHost /// The shell the navigator's screens are added to. Set by the application host before running. public View? Container { get; set; } + /// Adds as a full-size child of and gives it focus. public void Show(object view) { if (Container is not null && view is View concrete) @@ -24,6 +25,7 @@ public void Show(object view) } } + /// Removes from and disposes it. public void Remove(object view) { if (Container is not null && view is View concrete) diff --git a/src/SquidStd.Tui/Internal/TuiViewRegistry.cs b/src/SquidStd.Tui/Internal/TuiViewRegistry.cs index 323ffd58..e1e0b5c6 100644 --- a/src/SquidStd.Tui/Internal/TuiViewRegistry.cs +++ b/src/SquidStd.Tui/Internal/TuiViewRegistry.cs @@ -5,11 +5,13 @@ public sealed class TuiViewRegistry { private readonly Dictionary _map = new(); + /// Registers a mapping from to the that renders it. public void Map(Type viewModelType, Type viewType) { _map[viewModelType] = viewType; } + /// Returns the view type registered for , or throws if none is registered. public Type ViewTypeFor(Type viewModelType) { if (_map.TryGetValue(viewModelType, out var viewType)) diff --git a/src/SquidStd.Tui/Navigation/TuiNavigator.cs b/src/SquidStd.Tui/Navigation/TuiNavigator.cs index 9808f6d0..3c7a3d93 100644 --- a/src/SquidStd.Tui/Navigation/TuiNavigator.cs +++ b/src/SquidStd.Tui/Navigation/TuiNavigator.cs @@ -35,6 +35,11 @@ public async Task NavigateToAsync(CancellationToken cancellationToke view.Bind(viewModel); view.Initialize(); + if (_stack.Count > 0) + { + await _stack.Peek().ViewModel.OnDeactivatedAsync(); + } + _stack.Push(new Screen(viewModel, view)); _viewHost.Show(view); diff --git a/tests/SquidStd.Tests/Tui/Binding/ViewBinderAutoBindTests.cs b/tests/SquidStd.Tests/Tui/Binding/ViewBinderAutoBindTests.cs new file mode 100644 index 00000000..220936cf --- /dev/null +++ b/tests/SquidStd.Tests/Tui/Binding/ViewBinderAutoBindTests.cs @@ -0,0 +1,164 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using SquidStd.Tui.Binding; +using Terminal.Gui.ViewBase; +using Terminal.Gui.Views; + +namespace SquidStd.Tests.Tui.Binding; + +public partial class ViewBinderAutoBindTests +{ + private sealed partial class AutoBindViewModel : ObservableObject + { + [ObservableProperty] + private string _title = string.Empty; + + [ObservableProperty] + private string _name = string.Empty; + + // Wrong type on purpose — used by the "wrong type" skip-path test. + public int Count { get; } = 42; + + private bool _canSave; + + [RelayCommand(CanExecute = nameof(CanSave))] + private void Save() { } + + private bool CanSave() => _canSave; + + public void SetCanSave(bool value) + { + _canSave = value; + SaveCommand.NotifyCanExecuteChanged(); + } + } + + // ------------------------------------------------------------------- + // Label one-way + // ------------------------------------------------------------------- + + [Fact] + public void AutoBind_Label_AppliesInitialValue() + { + var vm = new AutoBindViewModel { Title = "Hello" }; + var parent = new View(); + var label = new Label { Id = "TitleLabel" }; + parent.Add(label); + + var binder = new ViewBinder(); + binder.AutoBind(parent, vm); + + Assert.Equal("Hello", label.Text); + } + + [Fact] + public void AutoBind_Label_UpdatesWhenVmPropertyChanges() + { + var vm = new AutoBindViewModel { Title = "initial" }; + var parent = new View(); + var label = new Label { Id = "TitleLabel" }; + parent.Add(label); + + var binder = new ViewBinder(); + binder.AutoBind(parent, vm); + + vm.Title = "updated"; + + Assert.Equal("updated", label.Text); + } + + // ------------------------------------------------------------------- + // TextField two-way (vm→field direction; writeback omitted headless) + // ------------------------------------------------------------------- + + [Fact] + public void AutoBind_TextField_AppliesInitialValue() + { + var vm = new AutoBindViewModel { Name = "squid" }; + var parent = new View(); + var field = new TextField { Id = "NameField" }; + parent.Add(field); + + var binder = new ViewBinder(); + binder.AutoBind(parent, vm); + + Assert.Equal("squid", field.Text); + } + + [Fact] + public void AutoBind_TextField_UpdatesFieldWhenVmPropertyChanges() + { + var vm = new AutoBindViewModel { Name = "before" }; + var parent = new View(); + var field = new TextField { Id = "NameField" }; + parent.Add(field); + + var binder = new ViewBinder(); + binder.AutoBind(parent, vm); + + vm.Name = "after"; + + Assert.Equal("after", field.Text); + } + + // ------------------------------------------------------------------- + // Button command — CanExecute drives Enabled + // ------------------------------------------------------------------- + + [Fact] + public void AutoBind_Button_TracksCanExecute() + { + var vm = new AutoBindViewModel(); // CanSave == false initially + var parent = new View(); + var button = new Button { Id = "SaveButton" }; + parent.Add(button); + + var binder = new ViewBinder(); + binder.AutoBind(parent, vm); + + Assert.False(button.Enabled); // initially disabled because CanSave == false + + vm.SetCanSave(true); + + Assert.True(button.Enabled); + + vm.SetCanSave(false); + + Assert.False(button.Enabled); + } + + // ------------------------------------------------------------------- + // Skip paths — no throw, no unintended side-effects + // ------------------------------------------------------------------- + + [Fact] + public void AutoBind_UnknownId_DoesNotThrow() + { + // "Unknown" maps to no VM member — should silently skip. + var vm = new AutoBindViewModel(); + var parent = new View(); + var label = new Label { Id = "UnknownLabel" }; + parent.Add(label); + + var binder = new ViewBinder(); + var ex = Record.Exception(() => binder.AutoBind(parent, vm)); + + Assert.Null(ex); + } + + [Fact] + public void AutoBind_WrongPropertyType_DoesNotThrow() + { + // "Count" is int, not string — BindStringByName returns early without throwing. + var vm = new AutoBindViewModel(); + var parent = new View(); + var label = new Label { Id = "CountLabel" }; + parent.Add(label); + + var binder = new ViewBinder(); + var ex = Record.Exception(() => binder.AutoBind(parent, vm)); + + Assert.Null(ex); + Assert.Equal(string.Empty, label.Text); // left untouched + } +} diff --git a/tests/SquidStd.Tests/Tui/Navigation/TuiNavigatorTests.cs b/tests/SquidStd.Tests/Tui/Navigation/TuiNavigatorTests.cs index 00b47e07..aeb072f9 100644 --- a/tests/SquidStd.Tests/Tui/Navigation/TuiNavigatorTests.cs +++ b/tests/SquidStd.Tests/Tui/Navigation/TuiNavigatorTests.cs @@ -106,4 +106,71 @@ public async Task Back_OnLastScreen_DoesNothing() Assert.Equal(1, navigator.Depth); // refuses to pop the root } + + private sealed class TrackingHomeViewModel : TuiViewModel + { + public int Activated { get; private set; } + public int Deactivated { get; private set; } + + public override ValueTask OnActivatedAsync() + { + Activated++; + return ValueTask.CompletedTask; + } + + public override ValueTask OnDeactivatedAsync() + { + Deactivated++; + return ValueTask.CompletedTask; + } + } + + private sealed class TrackingDetailViewModel : TuiViewModel + { + public int Activated { get; private set; } + public int Deactivated { get; private set; } + + public override ValueTask OnActivatedAsync() + { + Activated++; + return ValueTask.CompletedTask; + } + + public override ValueTask OnDeactivatedAsync() + { + Deactivated++; + return ValueTask.CompletedTask; + } + } + + [Fact] + public async Task ForwardThenBack_PairsActivationHooks_NoDoubleActivateWithoutDeactivate() + { + var container = new Container(); + container.Register(Reuse.Singleton); + container.Register(Reuse.Singleton); + container.Register(Reuse.Transient); + + var registry = new TuiViewRegistry(); + registry.Map(typeof(TrackingHomeViewModel), typeof(FakeView)); + registry.Map(typeof(TrackingDetailViewModel), typeof(FakeView)); + + var navigator = new TuiNavigator(container, registry, new FakeViewHost()); + var home = container.Resolve(); + var detail = container.Resolve(); + + await navigator.NavigateToAsync(); // home: A1 D0 + Assert.Equal(1, home.Activated); + Assert.Equal(0, home.Deactivated); + + await navigator.NavigateToAsync(); // home deactivated; detail A1 + Assert.Equal(1, home.Activated); + Assert.Equal(1, home.Deactivated); + Assert.Equal(1, detail.Activated); + + await navigator.BackAsync(); // detail D1; home reactivated + Assert.Equal(1, detail.Deactivated); + Assert.Equal(2, home.Activated); + Assert.Equal(1, home.Deactivated); // still 1 — every activate is now paired with a prior deactivate + } } From f672e15b1db3e6d3359a2bf234f7b1e720f5a90f Mon Sep 17 00:00:00 2001 From: Tom Date: Mon, 29 Jun 2026 21:12:11 +0200 Subject: [PATCH 15/39] feat(tui): add DSL node model and layout/bind-mode enums - Add BindMode enum (OneWay/TwoWay) under SquidStd.Tui.Types.Tui - Add StackOrientation enum (Vertical/Horizontal) under SquidStd.Tui.Types.Tui - Add abstract TuiNode base class under SquidStd.Tui.Dsl - Add LabelNode: one-way string property binding - Add TextFieldNode: string property binding with BindMode - Add ButtonNode: caption + ICommand expression binding - Add StackNode: orientation + typed children list - Add TuiNodeTests covering all four node types (4 tests, all green) --- src/SquidStd.Tui/Dsl/ButtonNode.cs | 23 ++++++++ src/SquidStd.Tui/Dsl/LabelNode.cs | 17 ++++++ src/SquidStd.Tui/Dsl/StackNode.cs | 21 ++++++++ src/SquidStd.Tui/Dsl/TextFieldNode.cs | 22 ++++++++ src/SquidStd.Tui/Dsl/TuiNode.cs | 7 +++ src/SquidStd.Tui/Types/Tui/BindMode.cs | 11 ++++ .../Types/Tui/StackOrientation.cs | 11 ++++ tests/SquidStd.Tests/Tui/Dsl/TuiNodeTests.cs | 53 +++++++++++++++++++ 8 files changed, 165 insertions(+) create mode 100644 src/SquidStd.Tui/Dsl/ButtonNode.cs create mode 100644 src/SquidStd.Tui/Dsl/LabelNode.cs create mode 100644 src/SquidStd.Tui/Dsl/StackNode.cs create mode 100644 src/SquidStd.Tui/Dsl/TextFieldNode.cs create mode 100644 src/SquidStd.Tui/Dsl/TuiNode.cs create mode 100644 src/SquidStd.Tui/Types/Tui/BindMode.cs create mode 100644 src/SquidStd.Tui/Types/Tui/StackOrientation.cs create mode 100644 tests/SquidStd.Tests/Tui/Dsl/TuiNodeTests.cs diff --git a/src/SquidStd.Tui/Dsl/ButtonNode.cs b/src/SquidStd.Tui/Dsl/ButtonNode.cs new file mode 100644 index 00000000..0ce44f00 --- /dev/null +++ b/src/SquidStd.Tui/Dsl/ButtonNode.cs @@ -0,0 +1,23 @@ +using System.Linq.Expressions; +using System.Windows.Input; + +namespace SquidStd.Tui.Dsl; + +/// A button whose activation runs a command resolved from the ViewModel. +public sealed class ButtonNode : TuiNode +{ + /// The button caption. + public string Caption { get; } + + /// The source property providing the command to run. + public Expression> Command { get; } + + public ButtonNode(string caption, Expression> command) + { + ArgumentException.ThrowIfNullOrEmpty(caption); + ArgumentNullException.ThrowIfNull(command); + + Caption = caption; + Command = command; + } +} diff --git a/src/SquidStd.Tui/Dsl/LabelNode.cs b/src/SquidStd.Tui/Dsl/LabelNode.cs new file mode 100644 index 00000000..b3686a1a --- /dev/null +++ b/src/SquidStd.Tui/Dsl/LabelNode.cs @@ -0,0 +1,17 @@ +using System.Linq.Expressions; + +namespace SquidStd.Tui.Dsl; + +/// A label whose text is one-way bound to a string property. +public sealed class LabelNode : TuiNode +{ + /// The source string property bound to the label text. + public Expression> Text { get; } + + public LabelNode(Expression> text) + { + ArgumentNullException.ThrowIfNull(text); + + Text = text; + } +} diff --git a/src/SquidStd.Tui/Dsl/StackNode.cs b/src/SquidStd.Tui/Dsl/StackNode.cs new file mode 100644 index 00000000..336118ab --- /dev/null +++ b/src/SquidStd.Tui/Dsl/StackNode.cs @@ -0,0 +1,21 @@ +using SquidStd.Tui.Types.Tui; + +namespace SquidStd.Tui.Dsl; + +/// A container that arranges its children vertically or horizontally. +public sealed class StackNode : TuiNode +{ + /// The direction children are arranged. + public StackOrientation Orientation { get; } + + /// The child nodes, in arrangement order. + public IReadOnlyList> Children { get; } + + public StackNode(StackOrientation orientation, IReadOnlyList> children) + { + ArgumentNullException.ThrowIfNull(children); + + Orientation = orientation; + Children = children; + } +} diff --git a/src/SquidStd.Tui/Dsl/TextFieldNode.cs b/src/SquidStd.Tui/Dsl/TextFieldNode.cs new file mode 100644 index 00000000..b37dd35b --- /dev/null +++ b/src/SquidStd.Tui/Dsl/TextFieldNode.cs @@ -0,0 +1,22 @@ +using System.Linq.Expressions; +using SquidStd.Tui.Types.Tui; + +namespace SquidStd.Tui.Dsl; + +/// An editable text field bound to a string property (two-way by default). +public sealed class TextFieldNode : TuiNode +{ + /// The source string property bound to the field text. + public Expression> Text { get; } + + /// The binding direction. + public BindMode Mode { get; } + + public TextFieldNode(Expression> text, BindMode mode) + { + ArgumentNullException.ThrowIfNull(text); + + Text = text; + Mode = mode; + } +} diff --git a/src/SquidStd.Tui/Dsl/TuiNode.cs b/src/SquidStd.Tui/Dsl/TuiNode.cs new file mode 100644 index 00000000..30a46f5c --- /dev/null +++ b/src/SquidStd.Tui/Dsl/TuiNode.cs @@ -0,0 +1,7 @@ +namespace SquidStd.Tui.Dsl; + +/// Immutable description of a UI element bound to . +/// The ViewModel the node's bindings target. +public abstract class TuiNode +{ +} diff --git a/src/SquidStd.Tui/Types/Tui/BindMode.cs b/src/SquidStd.Tui/Types/Tui/BindMode.cs new file mode 100644 index 00000000..2f76fe67 --- /dev/null +++ b/src/SquidStd.Tui/Types/Tui/BindMode.cs @@ -0,0 +1,11 @@ +namespace SquidStd.Tui.Types.Tui; + +/// Binding direction for a DSL node that supports both. +public enum BindMode +{ + /// Source-to-target only. + OneWay, + + /// Source-to-target and target-to-source. + TwoWay +} diff --git a/src/SquidStd.Tui/Types/Tui/StackOrientation.cs b/src/SquidStd.Tui/Types/Tui/StackOrientation.cs new file mode 100644 index 00000000..e0a0467d --- /dev/null +++ b/src/SquidStd.Tui/Types/Tui/StackOrientation.cs @@ -0,0 +1,11 @@ +namespace SquidStd.Tui.Types.Tui; + +/// Direction a Stack arranges its children. +public enum StackOrientation +{ + /// Children stacked top-to-bottom. + Vertical, + + /// Children placed left-to-right. + Horizontal +} diff --git a/tests/SquidStd.Tests/Tui/Dsl/TuiNodeTests.cs b/tests/SquidStd.Tests/Tui/Dsl/TuiNodeTests.cs new file mode 100644 index 00000000..72ea2eba --- /dev/null +++ b/tests/SquidStd.Tests/Tui/Dsl/TuiNodeTests.cs @@ -0,0 +1,53 @@ +using System.Windows.Input; +using CommunityToolkit.Mvvm.Input; +using SquidStd.Tui.Dsl; +using SquidStd.Tui.Internal; +using SquidStd.Tui.Types.Tui; + +namespace SquidStd.Tests.Tui.Dsl; + +public class TuiNodeTests +{ + private sealed class SampleViewModel + { + public string Title { get; set; } = string.Empty; + public ICommand Save { get; } = new RelayCommand(() => { }); + } + + [Fact] + public void LabelNode_CapturesTextExpression() + { + var node = new LabelNode(x => x.Title); + + Assert.Equal(nameof(SampleViewModel.Title), PropertyPath.NameOf(node.Text)); + } + + [Fact] + public void TextFieldNode_DefaultsToTwoWay() + { + var node = new TextFieldNode(x => x.Title, BindMode.TwoWay); + + Assert.Equal(BindMode.TwoWay, node.Mode); + Assert.Equal(nameof(SampleViewModel.Title), PropertyPath.NameOf(node.Text)); + } + + [Fact] + public void ButtonNode_CapturesCaptionAndCommand() + { + var node = new ButtonNode("Save", x => x.Save); + + Assert.Equal("Save", node.Caption); + Assert.NotNull(node.Command); + } + + [Fact] + public void StackNode_HoldsOrientationAndChildren() + { + var child = new LabelNode(x => x.Title); + var node = new StackNode(StackOrientation.Vertical, new TuiNode[] { child }); + + Assert.Equal(StackOrientation.Vertical, node.Orientation); + Assert.Single(node.Children); + Assert.Same(child, node.Children[0]); + } +} From be4b12b158070e332e69341812bceade3f8bbfb6 Mon Sep 17 00:00:00 2001 From: Tom Date: Mon, 29 Jun 2026 21:17:00 +0200 Subject: [PATCH 16/39] feat(tui): add UiFactory for building DSL nodes from typed lambdas --- src/SquidStd.Tui/Dsl/UiFactory.cs | 43 +++++++++++++++ .../SquidStd.Tests/Tui/Dsl/UiFactoryTests.cs | 54 +++++++++++++++++++ 2 files changed, 97 insertions(+) create mode 100644 src/SquidStd.Tui/Dsl/UiFactory.cs create mode 100644 tests/SquidStd.Tests/Tui/Dsl/UiFactoryTests.cs diff --git a/src/SquidStd.Tui/Dsl/UiFactory.cs b/src/SquidStd.Tui/Dsl/UiFactory.cs new file mode 100644 index 00000000..e6ad22ea --- /dev/null +++ b/src/SquidStd.Tui/Dsl/UiFactory.cs @@ -0,0 +1,43 @@ +using System.Linq.Expressions; +using System.Windows.Input; +using SquidStd.Tui.Types.Tui; + +namespace SquidStd.Tui.Dsl; + +/// +/// Builds DSL nodes from typed lambdas. The ViewModel type is fixed by the factory instance so the +/// member-expression lambdas (x => x.Property) infer without annotations. +/// +/// The ViewModel the built nodes bind to. +public sealed class UiFactory +{ + /// A label one-way bound to a string property. + public LabelNode Label(Expression> text) + { + return new LabelNode(text); + } + + /// A text field bound to a string property (two-way by default). + public TextFieldNode TextField(Expression> text, BindMode mode = BindMode.TwoWay) + { + return new TextFieldNode(text, mode); + } + + /// A button that runs the command resolved from the ViewModel. + public ButtonNode Button(string caption, Expression> command) + { + return new ButtonNode(caption, command); + } + + /// A vertical stack of child nodes. + public StackNode VStack(params TuiNode[] children) + { + return new StackNode(StackOrientation.Vertical, children); + } + + /// A horizontal stack of child nodes. + public StackNode HStack(params TuiNode[] children) + { + return new StackNode(StackOrientation.Horizontal, children); + } +} diff --git a/tests/SquidStd.Tests/Tui/Dsl/UiFactoryTests.cs b/tests/SquidStd.Tests/Tui/Dsl/UiFactoryTests.cs new file mode 100644 index 00000000..7d897068 --- /dev/null +++ b/tests/SquidStd.Tests/Tui/Dsl/UiFactoryTests.cs @@ -0,0 +1,54 @@ +using System.Windows.Input; +using CommunityToolkit.Mvvm.Input; +using SquidStd.Tui.Dsl; +using SquidStd.Tui.Internal; +using SquidStd.Tui.Types.Tui; + +namespace SquidStd.Tests.Tui.Dsl; + +public class UiFactoryTests +{ + private sealed class SampleViewModel + { + public string Title { get; set; } = string.Empty; + public string Name { get; set; } = string.Empty; + public ICommand Save { get; } = new RelayCommand(() => { }); + } + + private readonly UiFactory _ui = new(); + + [Fact] + public void Label_ProducesLabelNode() + { + var node = _ui.Label(x => x.Title); + + Assert.Equal(nameof(SampleViewModel.Title), PropertyPath.NameOf(node.Text)); + } + + [Fact] + public void TextField_DefaultsToTwoWay_AndOneWayIsExplicit() + { + Assert.Equal(BindMode.TwoWay, _ui.TextField(x => x.Name).Mode); + Assert.Equal(BindMode.OneWay, _ui.TextField(x => x.Name, BindMode.OneWay).Mode); + } + + [Fact] + public void Button_ProducesButtonNode() + { + var node = _ui.Button("Save", x => x.Save); + + Assert.Equal("Save", node.Caption); + } + + [Fact] + public void VStack_And_HStack_SetOrientationAndChildren() + { + var v = _ui.VStack(_ui.Label(x => x.Title), _ui.Label(x => x.Name)); + var h = _ui.HStack(_ui.Label(x => x.Title)); + + Assert.Equal(StackOrientation.Vertical, v.Orientation); + Assert.Equal(2, v.Children.Count); + Assert.Equal(StackOrientation.Horizontal, h.Orientation); + Assert.Single(h.Children); + } +} From 1e05947fbc84213437b3a1a742be5b7795b954b8 Mon Sep 17 00:00:00 2001 From: Tom Date: Mon, 29 Jun 2026 21:19:09 +0200 Subject: [PATCH 17/39] refactor(tui): add OnInitialize hook to TuiView for declarative composition --- src/SquidStd.Tui/TuiView.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/SquidStd.Tui/TuiView.cs b/src/SquidStd.Tui/TuiView.cs index 02926395..e7b1a306 100644 --- a/src/SquidStd.Tui/TuiView.cs +++ b/src/SquidStd.Tui/TuiView.cs @@ -30,9 +30,18 @@ void ITuiView.Bind(object viewModel) } void ITuiView.Initialize() + { + OnInitialize(_binder); + } + + /// + /// Builds the view. The default runs then ; the + /// declarative base overrides this to materialise a node tree instead. + /// + protected virtual void OnInitialize(ViewBinder binder) { BuildLayout(); - Bind(_binder); + Bind(binder); } /// Creates the Terminal.Gui widgets for this view. From 01da1b4516fdd3eb23ea1817fcddbb5d4e0f6c65 Mon Sep 17 00:00:00 2001 From: Tom Date: Mon, 29 Jun 2026 21:28:08 +0200 Subject: [PATCH 18/39] feat(tui): add materializer turning DSL nodes into bound Terminal.Gui views MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - add TuiNodeMaterializer in SquidStd.Tui.Dsl; walks TuiNode trees and produces View graphs - wire LabelNode via binder.OneWayText, TextFieldNode via TwoWay/OneWay binder overloads, ButtonNode via binder.Command - StackNode (Vertical/Horizontal) produces a container View using Pos.Bottom/Pos.Right and Dim.Fill for layout - TViewModel constrained to INotifyPropertyChanged so existing binder overloads resolve without new binding logic - add TuiNodeMaterializerTests covering VStack+Label one-way propagation and TextField two-way VM→widget; both pass headless - outer test class marked partial because nested ObservableObject partial class requires it (CommunityToolkit source generator) --- src/SquidStd.Tui/Dsl/TuiNodeMaterializer.cs | 92 +++++++++++++++++++ .../Tui/Dsl/TuiNodeMaterializerTests.cs | 49 ++++++++++ 2 files changed, 141 insertions(+) create mode 100644 src/SquidStd.Tui/Dsl/TuiNodeMaterializer.cs create mode 100644 tests/SquidStd.Tests/Tui/Dsl/TuiNodeMaterializerTests.cs diff --git a/src/SquidStd.Tui/Dsl/TuiNodeMaterializer.cs b/src/SquidStd.Tui/Dsl/TuiNodeMaterializer.cs new file mode 100644 index 00000000..b9abeba6 --- /dev/null +++ b/src/SquidStd.Tui/Dsl/TuiNodeMaterializer.cs @@ -0,0 +1,92 @@ +using System.ComponentModel; +using SquidStd.Tui.Binding; +using SquidStd.Tui.Types.Tui; +using Terminal.Gui.ViewBase; +using Terminal.Gui.Views; + +namespace SquidStd.Tui.Dsl; + +/// Turns a tree into a Terminal.Gui view graph, wiring each +/// node's binding through the supplied . +public sealed class TuiNodeMaterializer +{ + /// Materialises against , registering + /// bindings on , and returns the produced view. + public View Materialize(TuiNode node, TViewModel viewModel, ViewBinder binder) + where TViewModel : INotifyPropertyChanged + { + ArgumentNullException.ThrowIfNull(node); + ArgumentNullException.ThrowIfNull(viewModel); + ArgumentNullException.ThrowIfNull(binder); + + switch (node) + { + case LabelNode labelNode: + var label = new Label(); + binder.OneWayText(viewModel, labelNode.Text, label); + + return label; + + case TextFieldNode fieldNode: + var field = new TextField(); + + if (fieldNode.Mode == BindMode.TwoWay) + { + binder.TwoWay(viewModel, fieldNode.Text, field); + } + else + { + binder.OneWay(viewModel, fieldNode.Text, field, f => f.Text); + } + + return field; + + case ButtonNode buttonNode: + var button = new Button { Text = buttonNode.Caption }; + binder.Command(button, buttonNode.Command.Compile()(viewModel)); + + return button; + + case StackNode stackNode: + return MaterializeStack(stackNode, viewModel, binder); + + default: + throw new NotSupportedException($"Unsupported node type '{node.GetType().Name}'."); + } + } + + private View MaterializeStack(StackNode stack, TViewModel viewModel, ViewBinder binder) + where TViewModel : INotifyPropertyChanged + { + var container = new View + { + Width = Dim.Fill(), + Height = Dim.Fill() + }; + + View? previous = null; + + foreach (var childNode in stack.Children) + { + var child = Materialize(childNode, viewModel, binder); + + if (stack.Orientation == StackOrientation.Vertical) + { + child.X = 0; + child.Y = previous is null ? 0 : Pos.Bottom(previous); + child.Width = Dim.Fill(); + } + else + { + child.X = previous is null ? 0 : Pos.Right(previous); + child.Y = 0; + child.Height = Dim.Fill(); + } + + container.Add(child); + previous = child; + } + + return container; + } +} diff --git a/tests/SquidStd.Tests/Tui/Dsl/TuiNodeMaterializerTests.cs b/tests/SquidStd.Tests/Tui/Dsl/TuiNodeMaterializerTests.cs new file mode 100644 index 00000000..72f367cc --- /dev/null +++ b/tests/SquidStd.Tests/Tui/Dsl/TuiNodeMaterializerTests.cs @@ -0,0 +1,49 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using SquidStd.Tui.Binding; +using SquidStd.Tui.Dsl; +using Terminal.Gui.Views; + +namespace SquidStd.Tests.Tui.Dsl; + +public partial class TuiNodeMaterializerTests +{ + private sealed partial class SampleViewModel : ObservableObject + { + [ObservableProperty] + private string _title = "start"; + + [ObservableProperty] + private string _name = string.Empty; + } + + [Fact] + public void Materialize_VStack_ProducesChildrenAndAppliesOneWay() + { + var ui = new UiFactory(); + var vm = new SampleViewModel(); + var binder = new ViewBinder(); + var materializer = new TuiNodeMaterializer(); + + var root = materializer.Materialize(ui.VStack(ui.Label(x => x.Title)), vm, binder); + + var label = Assert.IsType diff --git a/src/SquidStd.Core/Types/Files/FileChangeKind.cs b/src/SquidStd.Core/Types/Files/FileChangeKind.cs index 256da868..fc24c5f8 100644 --- a/src/SquidStd.Core/Types/Files/FileChangeKind.cs +++ b/src/SquidStd.Core/Types/Files/FileChangeKind.cs @@ -1,7 +1,7 @@ namespace SquidStd.Core.Types.Files; /// -/// The kind of change observed on a watched file. +/// The kind of change observed on a watched file. /// public enum FileChangeKind { diff --git a/src/SquidStd.Core/Types/Health/HealthStatus.cs b/src/SquidStd.Core/Types/Health/HealthStatus.cs index 9dc66aed..d005a10e 100644 --- a/src/SquidStd.Core/Types/Health/HealthStatus.cs +++ b/src/SquidStd.Core/Types/Health/HealthStatus.cs @@ -1,7 +1,7 @@ namespace SquidStd.Core.Types.Health; /// -/// Overall health state of a check or report. +/// Overall health state of a check or report. /// public enum HealthStatus { diff --git a/src/SquidStd.Core/Types/LogLevelType.cs b/src/SquidStd.Core/Types/LogLevelType.cs index 6133c86c..43ef7f77 100644 --- a/src/SquidStd.Core/Types/LogLevelType.cs +++ b/src/SquidStd.Core/Types/LogLevelType.cs @@ -1,42 +1,42 @@ namespace SquidStd.Core.Types; /// -/// public enum LogLevelType : byte. +/// public enum LogLevelType : byte. /// public enum LogLevelType : byte { /// - /// No logging. + /// No logging. /// None = 0, /// - /// Trace level logging. + /// Trace level logging. /// Trace = 1, /// - /// Debug level logging. + /// Debug level logging. /// Debug = 2, /// - /// Information level logging. + /// Information level logging. /// Information = 3, /// - /// Warning level logging. + /// Warning level logging. /// Warning = 4, /// - /// Error level logging. + /// Error level logging. /// Error = 5, /// - /// Critical level logging. + /// Critical level logging. /// Critical = 6 } diff --git a/src/SquidStd.Core/Types/Metrics/MetricType.cs b/src/SquidStd.Core/Types/Metrics/MetricType.cs index 6202e72a..f59560ff 100644 --- a/src/SquidStd.Core/Types/Metrics/MetricType.cs +++ b/src/SquidStd.Core/Types/Metrics/MetricType.cs @@ -1,22 +1,22 @@ namespace SquidStd.Core.Types.Metrics; /// -/// Defines the semantic type of a metric sample. +/// Defines the semantic type of a metric sample. /// public enum MetricType { /// - /// A value that can go up or down. + /// A value that can go up or down. /// Gauge, /// - /// A cumulative value that only increases. + /// A cumulative value that only increases. /// Counter, /// - /// A value that represents a distribution bucket or aggregate. + /// A value that represents a distribution bucket or aggregate. /// Histogram } diff --git a/src/SquidStd.Core/Types/PlatformType.cs b/src/SquidStd.Core/Types/PlatformType.cs index 56571088..21f3f66c 100644 --- a/src/SquidStd.Core/Types/PlatformType.cs +++ b/src/SquidStd.Core/Types/PlatformType.cs @@ -1,7 +1,7 @@ namespace SquidStd.Core.Types; /// -/// Enumerates the supported platform types. +/// Enumerates the supported platform types. /// public enum PlatformType : byte { diff --git a/src/SquidStd.Core/Types/SquidStdLogRollingIntervalType.cs b/src/SquidStd.Core/Types/SquidStdLogRollingIntervalType.cs index 7584213a..9aa4830c 100644 --- a/src/SquidStd.Core/Types/SquidStdLogRollingIntervalType.cs +++ b/src/SquidStd.Core/Types/SquidStdLogRollingIntervalType.cs @@ -1,37 +1,37 @@ namespace SquidStd.Core.Types; /// -/// Defines file log rolling intervals. +/// Defines file log rolling intervals. /// public enum SquidStdLogRollingIntervalType { /// - /// Does not roll log files automatically. + /// Does not roll log files automatically. /// Infinite = 0, /// - /// Rolls log files every year. + /// Rolls log files every year. /// Year = 1, /// - /// Rolls log files every month. + /// Rolls log files every month. /// Month = 2, /// - /// Rolls log files every day. + /// Rolls log files every day. /// Day = 3, /// - /// Rolls log files every hour. + /// Rolls log files every hour. /// Hour = 4, /// - /// Rolls log files every minute. + /// Rolls log files every minute. /// Minute = 5 } diff --git a/src/SquidStd.Core/Utils/BuiltInRng.cs b/src/SquidStd.Core/Utils/BuiltInRng.cs index ad4d0580..7de1ccbf 100644 --- a/src/SquidStd.Core/Utils/BuiltInRng.cs +++ b/src/SquidStd.Core/Utils/BuiltInRng.cs @@ -3,8 +3,8 @@ namespace SquidStd.Core.Utils; /// -/// Ambient pseudo-random generator. Call with a fixed seed for reproducible -/// sequences (useful for deterministic simulations and game logic). +/// Ambient pseudo-random generator. Call with a fixed seed for reproducible +/// sequences (useful for deterministic simulations and game logic). /// public static class BuiltInRng { @@ -13,77 +13,57 @@ public static class BuiltInRng /// Replaces the generator with a new, non-deterministically seeded instance. public static void Reset() - { - Generator = new Random(); - } + => Generator = new(); /// Replaces the generator with one seeded for reproducible sequences. /// The seed value. public static void Reset(int seed) - { - Generator = new Random(seed); - } + => Generator = new(seed); /// Returns a non-negative random integer. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int Next() - { - return Generator.Next(); - } + => Generator.Next(); /// Returns a non-negative random integer below . /// Exclusive upper bound. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int Next(int maxValue) - { - return Generator.Next(maxValue); - } + => Generator.Next(maxValue); /// Returns a random integer in [minValue, minValue + count). /// Inclusive lower bound. /// Number of distinct values. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int Next(int minValue, int count) - { - return minValue + Generator.Next(count); - } + => minValue + Generator.Next(count); /// Returns a non-negative random long below . /// Exclusive upper bound. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static long Next(long maxValue) - { - return Generator.NextInt64(maxValue); - } + => Generator.NextInt64(maxValue); /// Returns a random long in [minValue, minValue + count). /// Inclusive lower bound. /// Number of distinct values. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static long Next(long minValue, long count) - { - return minValue + Generator.NextInt64(count); - } + => minValue + Generator.NextInt64(count); /// Returns a non-negative random long. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static long NextLong() - { - return Generator.NextInt64(); - } + => Generator.NextInt64(); /// Returns a random double in [0, 1). [MethodImpl(MethodImplOptions.AggressiveInlining)] public static double NextDouble() - { - return Generator.NextDouble(); - } + => Generator.NextDouble(); /// Fills the buffer with random bytes. /// The buffer to fill. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void NextBytes(Span buffer) - { - Generator.NextBytes(buffer); - } + => Generator.NextBytes(buffer); } diff --git a/src/SquidStd.Core/Utils/CryptoUtils.cs b/src/SquidStd.Core/Utils/CryptoUtils.cs index 431faa5c..7c78ea7d 100644 --- a/src/SquidStd.Core/Utils/CryptoUtils.cs +++ b/src/SquidStd.Core/Utils/CryptoUtils.cs @@ -4,8 +4,8 @@ namespace SquidStd.Core.Utils; /// -/// Authenticated symmetric encryption helpers built on AES-GCM. Produced payloads are laid out as -/// nonce (12 bytes) | tag (16 bytes) | ciphertext. +/// Authenticated symmetric encryption helpers built on AES-GCM. Produced payloads are laid out as +/// nonce (12 bytes) | tag (16 bytes) | ciphertext. /// public static class CryptoUtils { @@ -13,7 +13,7 @@ public static class CryptoUtils private const int TagSize = 16; /// - /// Generates a random symmetric key and returns it as a base64 string. + /// Generates a random symmetric key and returns it as a base64 string. /// /// Key length in bytes; must be 16, 24, or 32. /// The base64-encoded key. @@ -28,7 +28,7 @@ public static string GenerateKey(int sizeInBytes = 32) } /// - /// Encrypts a UTF-8 string with AES-GCM under the supplied key. + /// Encrypts a UTF-8 string with AES-GCM under the supplied key. /// /// The text to encrypt. /// The 16, 24, or 32 byte key. @@ -55,7 +55,7 @@ public static byte[] Encrypt(string plaintext, byte[] key) } /// - /// Decrypts a payload produced by back into its UTF-8 string. + /// Decrypts a payload produced by back into its UTF-8 string. /// /// The nonce | tag | ciphertext payload. /// The key used to encrypt the payload. diff --git a/src/SquidStd.Core/Utils/DirectoriesUtils.cs b/src/SquidStd.Core/Utils/DirectoriesUtils.cs index ffd8612b..7fd8e34a 100644 --- a/src/SquidStd.Core/Utils/DirectoriesUtils.cs +++ b/src/SquidStd.Core/Utils/DirectoriesUtils.cs @@ -1,23 +1,21 @@ namespace SquidStd.Core.Utils; /// -/// Utility methods for working with directories and file system operations +/// Utility methods for working with directories and file system operations /// public class DirectoriesUtils { /// - /// Gets files from the specified path recursively with optional extension filtering + /// Gets files from the specified path recursively with optional extension filtering /// /// Directory path to search /// File extensions to filter by (e.g., "*.txt", "*.json") /// Array of file paths matching the criteria public static string[] GetFiles(string path, params string[] extensions) - { - return GetFiles(path, true, extensions); - } + => GetFiles(path, true, extensions); /// - /// Gets files from the specified path with configurable recursion and extension filtering + /// Gets files from the specified path with configurable recursion and extension filtering /// /// Directory path to search /// Whether to search subdirectories recursively diff --git a/src/SquidStd.Core/Utils/HashUtils.cs b/src/SquidStd.Core/Utils/HashUtils.cs index 786a1215..7c63ae4e 100644 --- a/src/SquidStd.Core/Utils/HashUtils.cs +++ b/src/SquidStd.Core/Utils/HashUtils.cs @@ -4,7 +4,7 @@ namespace SquidStd.Core.Utils; /// -/// Provides password hashing and verification helpers using PBKDF2-SHA256. +/// Provides password hashing and verification helpers using PBKDF2-SHA256. /// public static class HashUtils { @@ -14,7 +14,7 @@ public static class HashUtils private const int DefaultSaltSize = 16; /// - /// Hashes a password using PBKDF2-SHA256 and returns a serialized payload. + /// Hashes a password using PBKDF2-SHA256 and returns a serialized payload. /// /// Plain password. /// Serialized hash payload. @@ -41,7 +41,7 @@ public static string HashPassword(string password) } /// - /// Verifies a plain password against a serialized PBKDF2-SHA256 payload. + /// Verifies a plain password against a serialized PBKDF2-SHA256 payload. /// /// Plain password. /// Serialized hash payload. diff --git a/src/SquidStd.Core/Utils/NetworkUtils.cs b/src/SquidStd.Core/Utils/NetworkUtils.cs index 7d0258b5..e32c695e 100644 --- a/src/SquidStd.Core/Utils/NetworkUtils.cs +++ b/src/SquidStd.Core/Utils/NetworkUtils.cs @@ -4,12 +4,12 @@ namespace SquidStd.Core.Utils; /// -/// Utility methods for network configuration parsing. +/// Utility methods for network configuration parsing. /// public static class NetworkUtils { /// - /// Enumerates local unicast endpoints matching the supplied endpoint address family. + /// Enumerates local unicast endpoints matching the supplied endpoint address family. /// /// The template endpoint supplying address family and port. /// The matching local endpoints. @@ -18,16 +18,17 @@ public static IEnumerable GetListeningAddresses(IPEndPoint endPoint) ArgumentNullException.ThrowIfNull(endPoint); return NetworkInterface.GetAllNetworkInterfaces() - .SelectMany(adapter => - adapter.GetIPProperties() - .UnicastAddresses - .Where(unicast => endPoint.AddressFamily == unicast.Address.AddressFamily) - .Select(unicast => new IPEndPoint(unicast.Address, endPoint.Port)) - ); + .SelectMany( + adapter => + adapter.GetIPProperties() + .UnicastAddresses + .Where(unicast => endPoint.AddressFamily == unicast.Address.AddressFamily) + .Select(unicast => new IPEndPoint(unicast.Address, endPoint.Port)) + ); } /// - /// Parses an IP address, treating "*" as every IPv4 interface. + /// Parses an IP address, treating "*" as every IPv4 interface. /// /// The IP address or "*". /// The parsed IP address. @@ -39,7 +40,7 @@ public static IPAddress ParseIpAddress(string ipAddress) } /// - /// Parses a comma-separated port list with optional ranges. + /// Parses a comma-separated port list with optional ranges. /// /// The ports string, such as "6666-6668,6669,8000". /// The parsed ports. diff --git a/src/SquidStd.Core/Utils/PlatformUtils.cs b/src/SquidStd.Core/Utils/PlatformUtils.cs index 2debaa05..29c7583c 100644 --- a/src/SquidStd.Core/Utils/PlatformUtils.cs +++ b/src/SquidStd.Core/Utils/PlatformUtils.cs @@ -3,12 +3,12 @@ namespace SquidStd.Core.Utils; /// -/// Provides utilities for detecting the current platform. +/// Provides utilities for detecting the current platform. /// public static class PlatformUtils { /// - /// Gets the current platform type. + /// Gets the current platform type. /// /// The detected platform type. public static PlatformType GetCurrentPlatform() @@ -27,29 +27,23 @@ public static PlatformType GetCurrentPlatform() } /// - /// Checks if the application is running on Linux. + /// Checks if the application is running on Linux. /// /// True if running on Linux, otherwise false. public static bool IsRunningOnLinux() - { - return OperatingSystem.IsLinux(); - } + => OperatingSystem.IsLinux(); /// - /// Checks if the application is running on macOS. + /// Checks if the application is running on macOS. /// /// True if running on macOS, otherwise false. public static bool IsRunningOnMacOS() - { - return OperatingSystem.IsMacOS(); - } + => OperatingSystem.IsMacOS(); /// - /// Checks if the application is running on Windows. + /// Checks if the application is running on Windows. /// /// True if running on Windows, otherwise false. public static bool IsRunningOnWindows() - { - return OperatingSystem.IsWindows(); - } + => OperatingSystem.IsWindows(); } diff --git a/src/SquidStd.Core/Utils/RandomUtils.cs b/src/SquidStd.Core/Utils/RandomUtils.cs index db77417c..c2e63eff 100644 --- a/src/SquidStd.Core/Utils/RandomUtils.cs +++ b/src/SquidStd.Core/Utils/RandomUtils.cs @@ -4,7 +4,7 @@ namespace SquidStd.Core.Utils; /// -/// Random value helpers built on , including dice and coin-flip mechanics. +/// Random value helpers built on , including dice and coin-flip mechanics. /// public static class RandomUtils { @@ -13,53 +13,41 @@ public static class RandomUtils /// Number of distinct values. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int Random(int from, int count) - { - return BuiltInRng.Next(from, count); - } + => BuiltInRng.Next(from, count); /// Returns a random integer below (sign-preserving). /// Exclusive bound; negative values mirror the range. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int Random(int count) - { - return count < 0 ? -BuiltInRng.Next(-count) : BuiltInRng.Next(count); - } + => count < 0 ? -BuiltInRng.Next(-count) : BuiltInRng.Next(count); /// Returns a random long in [from, from + count). /// Inclusive lower bound. /// Number of distinct values. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static long Random(long from, long count) - { - return BuiltInRng.Next(from, count); - } + => BuiltInRng.Next(from, count); /// Returns a random long below (sign-preserving). /// Exclusive bound; negative values mirror the range. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static long Random(long count) - { - return count < 0 ? -BuiltInRng.Next(-count) : BuiltInRng.Next(count); - } + => count < 0 ? -BuiltInRng.Next(-count) : BuiltInRng.Next(count); /// Fills the buffer with random bytes. /// The buffer to fill. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void RandomBytes(Span buffer) - { - BuiltInRng.NextBytes(buffer); - } + => BuiltInRng.NextBytes(buffer); /// Returns a random double in [0, 1). [MethodImpl(MethodImplOptions.AggressiveInlining)] public static double RandomDouble() - { - return BuiltInRng.NextDouble(); - } + => BuiltInRng.NextDouble(); /// - /// Counts heads across coin flips, stopping early once - /// heads are reached. + /// Counts heads across coin flips, stopping early once + /// heads are reached. /// /// Number of coins to flip. /// Cap on the number of heads to count. @@ -71,8 +59,8 @@ public static int CoinFlips(int amount, int maximum) while (amount > 0) { var num = amount >= 62 - ? (ulong)BuiltInRng.NextLong() - : (ulong)BuiltInRng.Next(1L << amount); + ? (ulong)BuiltInRng.NextLong() + : (ulong)BuiltInRng.Next(1L << amount); heads += BitOperations.PopCount(num); @@ -97,8 +85,8 @@ public static int CoinFlips(int amount) while (amount > 0) { var num = amount >= 62 - ? (ulong)BuiltInRng.NextLong() - : (ulong)BuiltInRng.Next(1L << amount); + ? (ulong)BuiltInRng.NextLong() + : (ulong)BuiltInRng.Next(1L << amount); heads += BitOperations.PopCount(num); diff --git a/src/SquidStd.Core/Utils/ResourceUtils.cs b/src/SquidStd.Core/Utils/ResourceUtils.cs index 55fb52f8..2559ba47 100644 --- a/src/SquidStd.Core/Utils/ResourceUtils.cs +++ b/src/SquidStd.Core/Utils/ResourceUtils.cs @@ -3,12 +3,12 @@ namespace SquidStd.Core.Utils; /// -/// Provides utilities for working with embedded resources. +/// Provides utilities for working with embedded resources. /// public static class ResourceUtils { /// - /// Converts a resource name to a file path format. + /// Converts a resource name to a file path format. /// /// The resource name to convert. /// The base namespace to remove. @@ -38,7 +38,7 @@ public static string ConvertResourceNameToPath(string resourceName, string baseN } /// - /// Copies all embedded resources from an assembly to a destination directory + /// Copies all embedded resources from an assembly to a destination directory /// /// The assembly containing the embedded resources /// The directory where resources will be copied @@ -72,7 +72,7 @@ public static void CopyEmbeddedToDirectory(Assembly assembly, string destination } /// - /// Converts an embedded resource name to a file path format + /// Converts an embedded resource name to a file path format /// /// The full embedded resource name /// The assembly prefix to remove from the resource name @@ -88,7 +88,7 @@ public static string EmbeddedNameToPath(string resourceName, string assemblyPref } /// - /// Converts an embedded resource name to a directory path structure + /// Converts an embedded resource name to a directory path structure /// /// The embedded resource name (e.g., "Assets.Fonts.DefaultUiFont.ttf") /// Optional base namespace to remove from the beginning @@ -128,7 +128,7 @@ public static string GetDirectoryPathFromResourceName(string resourceName, strin } /// - /// Gets an embedded resource as a byte array wrapped in Memory + /// Gets an embedded resource as a byte array wrapped in Memory /// /// The assembly containing the resource /// The full resource name @@ -145,7 +145,8 @@ public static Memory GetEmbeddedResourceByteArray(Assembly assembly, strin { // Try to find a partial match var resourceNames = assembly.GetManifestResourceNames(); - var matchingResource = resourceNames.FirstOrDefault(n => n.EndsWith( + var matchingResource = resourceNames.FirstOrDefault( + n => n.EndsWith( resourceName.Replace('/', '.').Replace('\\', '.'), StringComparison.Ordinal ) @@ -167,12 +168,12 @@ public static Memory GetEmbeddedResourceByteArray(Assembly assembly, strin using var memoryStream = new MemoryStream(); stream.CopyTo(memoryStream); - return new Memory(memoryStream.ToArray()); + return new(memoryStream.ToArray()); } } /// - /// Reads the content of an embedded resource as a byte array + /// Reads the content of an embedded resource as a byte array /// /// Resource path (e.g. "Assets/Templates/welcome.scriban") /// The assembly to search in (if null, uses current assembly) @@ -220,7 +221,7 @@ public static byte[] GetEmbeddedResourceContent(string resourcePath, Assembly as } /// - /// Gets a list of all files in a specific embedded directory + /// Gets a list of all files in a specific embedded directory /// /// The assembly to search in (if null, uses current assembly) /// Directory path to search (e.g. "Assets/Templates") @@ -255,7 +256,7 @@ public static List GetEmbeddedResourceFileNames( } /// - /// Gets a list of all embedded resources that match a given pattern + /// Gets a list of all embedded resources that match a given pattern /// /// The assembly to search in (if null, uses current assembly) /// Directory path to search (e.g. "Assets.Templates") @@ -288,19 +289,17 @@ public static string[] GetEmbeddedResourceNames(Assembly assembly = null, string } /// - /// Gets a stream for an embedded resource, inferring the assembly from . + /// Gets a stream for an embedded resource, inferring the assembly from . /// /// Any type defined in the target assembly. /// The full resource name or a path-style suffix. /// A stream for the resource. /// Thrown when the resource cannot be found. public static Stream GetEmbeddedResourceStream(string resourceName) - { - return GetEmbeddedResourceStream(typeof(TClass).Assembly, resourceName); - } + => GetEmbeddedResourceStream(typeof(TClass).Assembly, resourceName); /// - /// Gets a stream for an embedded resource. + /// Gets a stream for an embedded resource. /// /// The assembly containing the resource. /// The full resource name. @@ -317,7 +316,8 @@ public static Stream GetEmbeddedResourceStream(Assembly assembly, string resourc { // Try to find a partial match var resourceNames = assembly.GetManifestResourceNames(); - var matchingResource = resourceNames.FirstOrDefault(n => n.EndsWith( + var matchingResource = resourceNames.FirstOrDefault( + n => n.EndsWith( resourceName.Replace('/', '.').Replace('\\', '.'), StringComparison.Ordinal ) @@ -350,8 +350,8 @@ public static string GetEmbeddedResourceString(Assembly assembly, string resourc } /// - /// Converts an embedded resource name to a proper file name by extracting the last part after the final dot - /// and treating everything before it as directory structure + /// Converts an embedded resource name to a proper file name by extracting the last part after the final dot + /// and treating everything before it as directory structure /// /// The embedded resource name (e.g., "Assets.Fonts.DefaultUiFont.ttf") /// The file name with extension (e.g., "DefaultUiFont.ttf") @@ -380,7 +380,7 @@ public static string GetFileNameFromResourceName(string resourceName) } /// - /// Extracts the file name from an embedded resource path + /// Extracts the file name from an embedded resource path /// /// Full resource name /// File name without path @@ -395,23 +395,23 @@ public static string GetFileNameFromResourcePath(string resourceName) } /// - /// Reads the content of an embedded resource as a string. + /// Reads the content of an embedded resource as a string. /// /// The name of the resource to read. /// The assembly containing the resource. /// The content of the resource as a string. /// Thrown when the resource cannot be found in the specified assembly. /// - /// This method handles resource names that may contain either forward slashes (/) or - /// backslashes (\) by converting them to dots, which is the standard separator for - /// resource names in .NET assemblies. + /// This method handles resource names that may contain either forward slashes (/) or + /// backslashes (\) by converting them to dots, which is the standard separator for + /// resource names in .NET assemblies. /// public static string? ReadEmbeddedResource(string resourceName, Assembly assembly) { var resourcePath = resourceName.Replace('/', '.').Replace('\\', '.'); var fullResourceName = assembly.GetManifestResourceNames() - .FirstOrDefault(name => name.EndsWith(resourcePath, StringComparison.Ordinal)); + .FirstOrDefault(name => name.EndsWith(resourcePath, StringComparison.Ordinal)); if (fullResourceName == null) { diff --git a/src/SquidStd.Core/Utils/SslUtils.cs b/src/SquidStd.Core/Utils/SslUtils.cs index 37d7dcaf..c22e1333 100644 --- a/src/SquidStd.Core/Utils/SslUtils.cs +++ b/src/SquidStd.Core/Utils/SslUtils.cs @@ -3,12 +3,12 @@ namespace SquidStd.Core.Utils; /// -/// Helpers for loading X.509 certificates used to secure TLS endpoints. +/// Helpers for loading X.509 certificates used to secure TLS endpoints. /// public static class SslUtils { /// - /// Loads a certificate from a PEM file, optionally combining it with a separate private-key PEM. + /// Loads a certificate from a PEM file, optionally combining it with a separate private-key PEM. /// /// Path to the certificate PEM file. /// Optional path to the private-key PEM file. @@ -21,7 +21,7 @@ public static X509Certificate2 LoadFromPem(string certificatePath, string? priva } /// - /// Loads a certificate (with its private key) from a PKCS#12 / PFX file. + /// Loads a certificate (with its private key) from a PKCS#12 / PFX file. /// /// Path to the PFX file. /// Optional password protecting the PFX file. diff --git a/src/SquidStd.Core/Utils/StringUtils.cs b/src/SquidStd.Core/Utils/StringUtils.cs index 591f1d27..33c29575 100644 --- a/src/SquidStd.Core/Utils/StringUtils.cs +++ b/src/SquidStd.Core/Utils/StringUtils.cs @@ -5,22 +5,22 @@ namespace SquidStd.Core.Utils; /// -/// Provides utility methods for string operations, including various case conversion methods. +/// Provides utility methods for string operations, including various case conversion methods. /// public static partial class StringUtils { private static readonly Regex WordSplitterRegex = WordSplitter(); /// - /// Converts a string to camelCase. + /// Converts a string to camelCase. /// /// The string to convert to camelCase. /// A camelCase version of the input string. /// Thrown when the input text is null or empty. /// - /// "HelloWorld" becomes "helloWorld" - /// "API_RESPONSE" becomes "apiResponse" - /// "user-id" becomes "userId" + /// "HelloWorld" becomes "helloWorld" + /// "API_RESPONSE" becomes "apiResponse" + /// "user-id" becomes "userId" /// public static string ToCamelCase(string text) { @@ -51,13 +51,13 @@ public static string ToCamelCase(string text) } /// - /// Converts a string to Dot Case. + /// Converts a string to Dot Case. /// /// The string to convert to Dot Case. /// A Dot Case version of the input string. /// - /// "HelloWorld" becomes "hello.world" - /// "API_RESPONSE" becomes "api.response" + /// "HelloWorld" becomes "hello.world" + /// "API_RESPONSE" becomes "api.response" /// public static string ToDotCase(string text) { @@ -96,15 +96,15 @@ public static string ToDotCase(string text) } /// - /// Converts a string to kebab-case. + /// Converts a string to kebab-case. /// /// The string to convert to kebab-case. /// A kebab-case version of the input string. /// Thrown when the input text is null or empty. /// - /// "HelloWorld" becomes "hello-world" - /// "API_RESPONSE" becomes "api-response" - /// "userId" becomes "user-id" + /// "HelloWorld" becomes "hello-world" + /// "API_RESPONSE" becomes "api-response" + /// "userId" becomes "user-id" /// public static string ToKebabCase(string text) { @@ -143,15 +143,15 @@ public static string ToKebabCase(string text) } /// - /// Converts a string to PascalCase. + /// Converts a string to PascalCase. /// /// The string to convert to PascalCase. /// A PascalCase version of the input string. /// Thrown when the input text is null or empty. /// - /// "hello_world" becomes "HelloWorld" - /// "api-response" becomes "ApiResponse" - /// "userId" becomes "UserId" + /// "hello_world" becomes "HelloWorld" + /// "api-response" becomes "ApiResponse" + /// "userId" becomes "UserId" /// public static string ToPascalCase(string text) { @@ -182,13 +182,13 @@ public static string ToPascalCase(string text) } /// - /// Converts a string to Path Case. + /// Converts a string to Path Case. /// /// The string to convert to Path Case. /// A Path Case version of the input string. /// - /// "HelloWorld" becomes "hello/world" - /// "API_RESPONSE" becomes "api/response" + /// "HelloWorld" becomes "hello/world" + /// "API_RESPONSE" becomes "api/response" /// public static string ToPathCase(string text) { @@ -227,13 +227,13 @@ public static string ToPathCase(string text) } /// - /// Converts a string to Sentence Case. + /// Converts a string to Sentence Case. /// /// The string to convert to Sentence Case. /// A Sentence Case version of the input string. /// - /// "hello world" becomes "Hello world" - /// "API_RESPONSE" becomes "Api response" + /// "hello world" becomes "Hello world" + /// "API_RESPONSE" becomes "Api response" /// public static string ToSentenceCase(string text) { @@ -284,15 +284,15 @@ public static string ToSentenceCase(string text) } /// - /// Converts a string from camelCase or PascalCase to snake_case. + /// Converts a string from camelCase or PascalCase to snake_case. /// /// The string to convert to snake_case. /// A snake_case version of the input string. /// Thrown when the input text is null or empty. /// - /// "HelloWorld" becomes "hello_world" - /// "APIResponse" becomes "api_response" - /// "userId" becomes "user_id" + /// "HelloWorld" becomes "hello_world" + /// "APIResponse" becomes "api_response" + /// "userId" becomes "user_id" /// public static string ToSnakeCase(string text) { @@ -331,15 +331,15 @@ public static string ToSnakeCase(string text) } /// - /// Converts a string to Title Case. + /// Converts a string to Title Case. /// /// The string to convert to Title Case. /// A Title Case version of the input string. /// Thrown when the input text is null or empty. /// - /// "hello_world" becomes "Hello World" - /// "API_RESPONSE" becomes "Api Response" - /// "user-id" becomes "User Id" + /// "hello_world" becomes "Hello World" + /// "API_RESPONSE" becomes "Api Response" + /// "user-id" becomes "User Id" /// public static string ToTitleCase(string text) { @@ -373,13 +373,13 @@ public static string ToTitleCase(string text) } /// - /// Converts a string to Train Case (Pascal Case with hyphens). + /// Converts a string to Train Case (Pascal Case with hyphens). /// /// The string to convert to Train Case. /// A Train Case version of the input string. /// - /// "hello_world" becomes "Hello-World" - /// "apiResponse" becomes "Api-Response" + /// "hello_world" becomes "Hello-World" + /// "apiResponse" becomes "Api-Response" /// public static string ToTrainCase(string text) { @@ -418,20 +418,18 @@ public static string ToTrainCase(string text) } /// - /// Converts a string to UPPER_SNAKE_CASE (screaming snake case). + /// Converts a string to UPPER_SNAKE_CASE (screaming snake case). /// /// The string to convert to UPPER_SNAKE_CASE. /// An UPPER_SNAKE_CASE version of the input string. /// Thrown when the input text is null or empty. /// - /// "HelloWorld" becomes "HELLO_WORLD" - /// "apiResponse" becomes "API_RESPONSE" - /// "user-id" becomes "USER_ID" + /// "HelloWorld" becomes "HELLO_WORLD" + /// "apiResponse" becomes "API_RESPONSE" + /// "user-id" becomes "USER_ID" /// public static string ToUpperSnakeCase(string text) - { - return ToSnakeCase(text).ToUpperInvariant(); - } + => ToSnakeCase(text).ToUpperInvariant(); [GeneratedRegex(@"[\s_-]|(?<=[a-z])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])", RegexOptions.Compiled)] private static partial Regex WordSplitter(); diff --git a/src/SquidStd.Core/Utils/VersionUtils.cs b/src/SquidStd.Core/Utils/VersionUtils.cs index 45938b01..60af9eb1 100644 --- a/src/SquidStd.Core/Utils/VersionUtils.cs +++ b/src/SquidStd.Core/Utils/VersionUtils.cs @@ -3,21 +3,19 @@ namespace SquidStd.Core.Utils; /// -/// Provides utility methods for reading assembly version metadata. +/// Provides utility methods for reading assembly version metadata. /// public static class VersionUtils { /// - /// Gets the informational version for the LyLy.Core assembly. + /// Gets the informational version for the LyLy.Core assembly. /// /// The package version declared for LyLy.Core. public static string GetVersion() - { - return GetVersion(typeof(VersionUtils).Assembly); - } + => GetVersion(typeof(VersionUtils).Assembly); /// - /// Gets the informational version for the specified assembly. + /// Gets the informational version for the specified assembly. /// /// The assembly to read version metadata from. /// The assembly informational version, or the assembly version when informational metadata is unavailable. @@ -26,7 +24,7 @@ public static string GetVersion(Assembly assembly) ArgumentNullException.ThrowIfNull(assembly); var informationalVersion = assembly.GetCustomAttribute() - ?.InformationalVersion; + ?.InformationalVersion; if (!string.IsNullOrWhiteSpace(informationalVersion)) { diff --git a/src/SquidStd.Core/Yaml/YamlUtils.cs b/src/SquidStd.Core/Yaml/YamlUtils.cs index 7fd5e36f..1e8dd7ca 100644 --- a/src/SquidStd.Core/Yaml/YamlUtils.cs +++ b/src/SquidStd.Core/Yaml/YamlUtils.cs @@ -3,21 +3,21 @@ namespace SquidStd.Core.Yaml; /// -/// Provides YAML serialization helpers. +/// Provides YAML serialization helpers. /// public static class YamlUtils { private static readonly ISerializer Serializer = new SerializerBuilder() - .DisableAliases() - .WithIndentedSequences() - .Build(); + .DisableAliases() + .WithIndentedSequences() + .Build(); private static readonly IDeserializer Deserializer = new DeserializerBuilder() - .IgnoreUnmatchedProperties() - .Build(); + .IgnoreUnmatchedProperties() + .Build(); /// - /// Deserializes YAML text using reflection-based metadata. + /// Deserializes YAML text using reflection-based metadata. /// /// The YAML text to deserialize. /// The target type. @@ -31,7 +31,7 @@ public static T Deserialize(string yaml) } /// - /// Deserializes YAML text to the specified runtime type. + /// Deserializes YAML text to the specified runtime type. /// /// The YAML text to deserialize. /// The target type. @@ -46,7 +46,7 @@ public static object Deserialize(string yaml, Type type) } /// - /// Deserializes YAML from a file using reflection-based metadata. + /// Deserializes YAML from a file using reflection-based metadata. /// /// The YAML file path. /// The target type. @@ -61,7 +61,7 @@ public static T DeserializeFromFile(string filePath) } /// - /// Deserializes a top-level YAML section to the specified runtime type. + /// Deserializes a top-level YAML section to the specified runtime type. /// /// The YAML document. /// The top-level section name. @@ -84,7 +84,7 @@ public static T DeserializeFromFile(string filePath) } /// - /// Serializes an object to YAML using reflection-based metadata. + /// Serializes an object to YAML using reflection-based metadata. /// /// The object to serialize. /// The source type. @@ -97,7 +97,7 @@ public static string Serialize(T obj) } /// - /// Serializes top-level YAML sections. + /// Serializes top-level YAML sections. /// /// The section map to serialize. /// The serialized YAML document. @@ -109,7 +109,7 @@ public static string SerializeSections(IReadOnlyDictionary secti } /// - /// Serializes an object to a YAML file using reflection-based metadata. + /// Serializes an object to a YAML file using reflection-based metadata. /// /// The object to serialize. /// The output YAML file path. diff --git a/src/SquidStd.Crypto/Pgp/Data/PgpDecryptionResult.cs b/src/SquidStd.Crypto/Pgp/Data/PgpDecryptionResult.cs index 417836b4..16d2eee8 100644 --- a/src/SquidStd.Crypto/Pgp/Data/PgpDecryptionResult.cs +++ b/src/SquidStd.Crypto/Pgp/Data/PgpDecryptionResult.cs @@ -1,7 +1,7 @@ namespace SquidStd.Crypto.Pgp.Data; /// -/// Outcome of decrypt-and-verify: the recovered plaintext, whether the message carried a signature, and -/// whether that signature validated against a keyring public key. +/// Outcome of decrypt-and-verify: the recovered plaintext, whether the message carried a signature, and +/// whether that signature validated against a keyring public key. /// public sealed record PgpDecryptionResult(byte[] Data, bool IsSigned, bool IsValid); diff --git a/src/SquidStd.Crypto/Pgp/Data/PgpKey.cs b/src/SquidStd.Crypto/Pgp/Data/PgpKey.cs index 2fd65dcd..114e6e00 100644 --- a/src/SquidStd.Crypto/Pgp/Data/PgpKey.cs +++ b/src/SquidStd.Crypto/Pgp/Data/PgpKey.cs @@ -3,8 +3,8 @@ namespace SquidStd.Crypto.Pgp.Data; /// -/// An OpenPGP key as held in the keyring: identity, key id, fingerprint, the armored public block, and -/// optionally the armored secret block. Metadata is derived from the public key material. +/// An OpenPGP key as held in the keyring: identity, key id, fingerprint, the armored public block, and +/// optionally the armored secret block. Metadata is derived from the public key material. /// public sealed record PgpKey( string Identity, diff --git a/src/SquidStd.Crypto/Pgp/Data/PgpKeyOptions.cs b/src/SquidStd.Crypto/Pgp/Data/PgpKeyOptions.cs index 69f62e86..8a1bd2bf 100644 --- a/src/SquidStd.Crypto/Pgp/Data/PgpKeyOptions.cs +++ b/src/SquidStd.Crypto/Pgp/Data/PgpKeyOptions.cs @@ -3,7 +3,7 @@ namespace SquidStd.Crypto.Pgp.Data; /// -/// Options controlling key generation: algorithm, key size, and an optional validity period. +/// Options controlling key generation: algorithm, key size, and an optional validity period. /// public sealed class PgpKeyOptions { diff --git a/src/SquidStd.Crypto/Pgp/Data/PgpVerificationResult.cs b/src/SquidStd.Crypto/Pgp/Data/PgpVerificationResult.cs index 7fe6fc3d..612b5dde 100644 --- a/src/SquidStd.Crypto/Pgp/Data/PgpVerificationResult.cs +++ b/src/SquidStd.Crypto/Pgp/Data/PgpVerificationResult.cs @@ -1,7 +1,7 @@ namespace SquidStd.Crypto.Pgp.Data; /// -/// Outcome of verifying a signed message: whether the signature validated against a keyring public key, -/// plus the recovered content. PgpCore reports pass/fail only — no signer attribution is exposed. +/// Outcome of verifying a signed message: whether the signature validated against a keyring public key, +/// plus the recovered content. PgpCore reports pass/fail only — no signer attribution is exposed. /// public sealed record PgpVerificationResult(bool IsValid, byte[] Data); diff --git a/src/SquidStd.Crypto/Pgp/Extensions/RegisterPgpExtensions.cs b/src/SquidStd.Crypto/Pgp/Extensions/RegisterPgpExtensions.cs index 56153d0b..cb57b89c 100644 --- a/src/SquidStd.Crypto/Pgp/Extensions/RegisterPgpExtensions.cs +++ b/src/SquidStd.Crypto/Pgp/Extensions/RegisterPgpExtensions.cs @@ -11,8 +11,8 @@ public static class RegisterPgpExtensions extension(IContainer container) { /// - /// Registers the PGP keyring, service, and the chosen key store (all singletons). The keyring is not - /// auto-loaded; call at startup if persistence is desired. + /// Registers the PGP keyring, service, and the chosen key store (all singletons). The keyring is not + /// auto-loaded; call at startup if persistence is desired. /// /// Builds the from the resolver. /// The same container for chaining. diff --git a/src/SquidStd.Crypto/Pgp/Interfaces/IPgpKeyStore.cs b/src/SquidStd.Crypto/Pgp/Interfaces/IPgpKeyStore.cs index b5f4ebbb..1fcecefc 100644 --- a/src/SquidStd.Crypto/Pgp/Interfaces/IPgpKeyStore.cs +++ b/src/SquidStd.Crypto/Pgp/Interfaces/IPgpKeyStore.cs @@ -3,7 +3,7 @@ namespace SquidStd.Crypto.Pgp.Interfaces; /// -/// Persistence backend for a set of PGP keys. Implementations decide the on-disk representation. +/// Persistence backend for a set of PGP keys. Implementations decide the on-disk representation. /// public interface IPgpKeyStore { diff --git a/src/SquidStd.Crypto/Pgp/Interfaces/IPgpKeyring.cs b/src/SquidStd.Crypto/Pgp/Interfaces/IPgpKeyring.cs index bd2917f6..bdcc4a9c 100644 --- a/src/SquidStd.Crypto/Pgp/Interfaces/IPgpKeyring.cs +++ b/src/SquidStd.Crypto/Pgp/Interfaces/IPgpKeyring.cs @@ -3,8 +3,8 @@ namespace SquidStd.Crypto.Pgp.Interfaces; /// -/// In-memory collection of PGP keys, indexed by identity, key id, and fingerprint. Crypto operations look -/// recipients and signers up here by identity. +/// In-memory collection of PGP keys, indexed by identity, key id, and fingerprint. Crypto operations look +/// recipients and signers up here by identity. /// public interface IPgpKeyring { diff --git a/src/SquidStd.Crypto/Pgp/Interfaces/IPgpService.cs b/src/SquidStd.Crypto/Pgp/Interfaces/IPgpService.cs index cdf07fcc..07b249c1 100644 --- a/src/SquidStd.Crypto/Pgp/Interfaces/IPgpService.cs +++ b/src/SquidStd.Crypto/Pgp/Interfaces/IPgpService.cs @@ -3,8 +3,8 @@ namespace SquidStd.Crypto.Pgp.Interfaces; /// -/// OpenPGP operations over the keyring: key generation, encrypt/decrypt, sign/verify, and the combined -/// encrypt+sign / decrypt+verify flows. Recipients and signers are resolved from the keyring by identity. +/// OpenPGP operations over the keyring: key generation, encrypt/decrypt, sign/verify, and the combined +/// encrypt+sign / decrypt+verify flows. Recipients and signers are resolved from the keyring by identity. /// public interface IPgpService { @@ -16,7 +16,10 @@ public interface IPgpService /// Encrypts a stream for the recipient, writing an armored message to . Task EncryptForAsync( - string recipientIdentity, Stream input, Stream output, CancellationToken cancellationToken = default + string recipientIdentity, + Stream input, + Stream output, + CancellationToken cancellationToken = default ); /// Decrypts an armored message using the matching keyring secret key and passphrase. @@ -27,24 +30,36 @@ Task EncryptForAsync( /// Encrypts for the recipient and signs it with the signer's secret key. Task EncryptAndSignForAsync( - string recipientIdentity, byte[] data, string signerIdentity, string signerPassphrase, + string recipientIdentity, + byte[] data, + string signerIdentity, + string signerPassphrase, CancellationToken cancellationToken = default ); /// Encrypts and signs a stream for the recipient. Task EncryptAndSignForAsync( - string recipientIdentity, Stream input, Stream output, string signerIdentity, string signerPassphrase, + string recipientIdentity, + Stream input, + Stream output, + string signerIdentity, + string signerPassphrase, CancellationToken cancellationToken = default ); /// Decrypts an armored message and reports whether it was signed and whether the signature validated. Task DecryptAndVerifyAsync( - string armored, string passphrase, CancellationToken cancellationToken = default + string armored, + string passphrase, + CancellationToken cancellationToken = default ); /// Produces an armored signed message that embeds . Task SignAsync( - byte[] data, string signerIdentity, string passphrase, CancellationToken cancellationToken = default + byte[] data, + string signerIdentity, + string passphrase, + CancellationToken cancellationToken = default ); /// Verifies an armored signed message against the keyring, recovering the embedded content. diff --git a/src/SquidStd.Crypto/Pgp/Internal/PgpKeyFactory.cs b/src/SquidStd.Crypto/Pgp/Internal/PgpKeyFactory.cs index 5838b962..082c3dd5 100644 --- a/src/SquidStd.Crypto/Pgp/Internal/PgpKeyFactory.cs +++ b/src/SquidStd.Crypto/Pgp/Internal/PgpKeyFactory.cs @@ -8,8 +8,8 @@ namespace SquidStd.Crypto.Pgp.Internal; /// -/// Builds a from armored key material by reading metadata off the public master key. -/// Shared by key generation, keyring import, and key-store loading. +/// Builds a from armored key material by reading metadata off the public master key. +/// Shared by key generation, keyring import, and key-store loading. /// internal static class PgpKeyFactory { @@ -25,7 +25,7 @@ public static PgpKey FromArmored(string publicArmored, string? privateArmored) var validSeconds = master.GetValidSeconds(); DateTimeOffset? expiresUtc = validSeconds > 0 ? createdUtc.AddSeconds(validSeconds) : null; - return new PgpKey( + return new( identity, keyId, fingerprint, @@ -51,12 +51,10 @@ private static string FirstUserId(PgpPublicKey master) } private static PgpKeyAlgorithm MapAlgorithm(PublicKeyAlgorithmTag tag) - { - return tag switch + => tag switch { PublicKeyAlgorithmTag.RsaGeneral or PublicKeyAlgorithmTag.RsaEncrypt or PublicKeyAlgorithmTag.RsaSign => PgpKeyAlgorithm.Rsa, _ => PgpKeyAlgorithm.Rsa }; - } } diff --git a/src/SquidStd.Crypto/Pgp/Internal/PgpKeyStoreCodec.cs b/src/SquidStd.Crypto/Pgp/Internal/PgpKeyStoreCodec.cs index 24d3fcad..83054767 100644 --- a/src/SquidStd.Crypto/Pgp/Internal/PgpKeyStoreCodec.cs +++ b/src/SquidStd.Crypto/Pgp/Internal/PgpKeyStoreCodec.cs @@ -4,8 +4,8 @@ namespace SquidStd.Crypto.Pgp.Internal; /// -/// Encodes a set of keys into a single byte blob (one base64 record per line) and back. Only armored -/// material is stored; metadata is re-derived on load. +/// Encodes a set of keys into a single byte blob (one base64 record per line) and back. Only armored +/// material is stored; metadata is re-derived on load. /// internal static class PgpKeyStoreCodec { @@ -17,8 +17,8 @@ public static byte[] Encode(IReadOnlyCollection keys) { var pub = Convert.ToBase64String(Encoding.UTF8.GetBytes(key.PublicArmored)); var sec = key.PrivateArmored is null - ? string.Empty - : Convert.ToBase64String(Encoding.UTF8.GetBytes(key.PrivateArmored)); + ? string.Empty + : Convert.ToBase64String(Encoding.UTF8.GetBytes(key.PrivateArmored)); builder.Append(pub).Append('\t').Append(sec).Append('\n'); } @@ -34,9 +34,9 @@ public static IReadOnlyList Decode(byte[] blob) { var parts = line.Split('\t'); var publicArmored = Encoding.UTF8.GetString(Convert.FromBase64String(parts[0])); - string? privateArmored = parts.Length > 1 && parts[1].Length > 0 - ? Encoding.UTF8.GetString(Convert.FromBase64String(parts[1])) - : null; + var privateArmored = parts.Length > 1 && parts[1].Length > 0 + ? Encoding.UTF8.GetString(Convert.FromBase64String(parts[1])) + : null; result.Add(PgpKeyFactory.FromArmored(publicArmored, privateArmored)); } diff --git a/src/SquidStd.Crypto/Pgp/Services/AesGcmPgpKeyStore.cs b/src/SquidStd.Crypto/Pgp/Services/AesGcmPgpKeyStore.cs index f595a789..7090f1cf 100644 --- a/src/SquidStd.Crypto/Pgp/Services/AesGcmPgpKeyStore.cs +++ b/src/SquidStd.Crypto/Pgp/Services/AesGcmPgpKeyStore.cs @@ -6,8 +6,8 @@ namespace SquidStd.Crypto.Pgp.Services; /// -/// Key store that serializes the keyring to a single file encrypted at rest with the application key via -/// . +/// Key store that serializes the keyring to a single file encrypted at rest with the application key via +/// . /// public sealed class AesGcmPgpKeyStore : IPgpKeyStore { @@ -30,6 +30,7 @@ public async Task SaveAsync(IReadOnlyCollection keys, CancellationToken var protectedBytes = _protector.Protect(plaintext); var directory = Path.GetDirectoryName(_path); + if (!string.IsNullOrEmpty(directory)) { Directory.CreateDirectory(directory); diff --git a/src/SquidStd.Crypto/Pgp/Services/FilePgpKeyStore.cs b/src/SquidStd.Crypto/Pgp/Services/FilePgpKeyStore.cs index e71d2c1c..55568d23 100644 --- a/src/SquidStd.Crypto/Pgp/Services/FilePgpKeyStore.cs +++ b/src/SquidStd.Crypto/Pgp/Services/FilePgpKeyStore.cs @@ -6,8 +6,8 @@ namespace SquidStd.Crypto.Pgp.Services; /// -/// Key store backed by a directory of armored .asc files (one public, optionally one secret, per -/// key). gpg-interoperable. +/// Key store backed by a directory of armored .asc files (one public, optionally one secret, per +/// key). gpg-interoperable. /// public sealed class FilePgpKeyStore : IPgpKeyStore { @@ -31,20 +31,20 @@ public async Task SaveAsync(IReadOnlyCollection keys, CancellationToken { var stem = Stem(key); await File.WriteAllTextAsync( - Path.Combine(_directory, stem + PublicSuffix), - key.PublicArmored, - cancellationToken - ) - .ConfigureAwait(false); + Path.Combine(_directory, stem + PublicSuffix), + key.PublicArmored, + cancellationToken + ) + .ConfigureAwait(false); if (key.PrivateArmored is not null) { await File.WriteAllTextAsync( - Path.Combine(_directory, stem + SecretSuffix), - key.PrivateArmored, - cancellationToken - ) - .ConfigureAwait(false); + Path.Combine(_directory, stem + SecretSuffix), + key.PrivateArmored, + cancellationToken + ) + .ConfigureAwait(false); } } } @@ -65,9 +65,9 @@ public async Task> LoadAsync(CancellationToken cancellatio var publicArmored = await File.ReadAllTextAsync(publicPath, cancellationToken).ConfigureAwait(false); var secretPath = Path.Combine(_directory, stem + SecretSuffix); - string? secretArmored = File.Exists(secretPath) - ? await File.ReadAllTextAsync(secretPath, cancellationToken).ConfigureAwait(false) - : null; + var secretArmored = File.Exists(secretPath) + ? await File.ReadAllTextAsync(secretPath, cancellationToken).ConfigureAwait(false) + : null; result.Add(PgpKeyFactory.FromArmored(publicArmored, secretArmored)); } @@ -78,6 +78,7 @@ public async Task> LoadAsync(CancellationToken cancellatio private static string Stem(PgpKey key) { var safeIdentity = new StringBuilder(key.Identity.Length); + foreach (var ch in key.Identity) { safeIdentity.Append(char.IsLetterOrDigit(ch) ? ch : '_'); diff --git a/src/SquidStd.Crypto/Pgp/Services/PgpKeyring.cs b/src/SquidStd.Crypto/Pgp/Services/PgpKeyring.cs index 8d258635..efc8ce14 100644 --- a/src/SquidStd.Crypto/Pgp/Services/PgpKeyring.cs +++ b/src/SquidStd.Crypto/Pgp/Services/PgpKeyring.cs @@ -9,7 +9,7 @@ namespace SquidStd.Crypto.Pgp.Services; /// -/// Thread-safe in-memory keyring indexed by identity, key id, and fingerprint. +/// Thread-safe in-memory keyring indexed by identity, key id, and fingerprint. /// public sealed class PgpKeyring : IPgpKeyring { @@ -25,8 +25,8 @@ public PgpKey Import(string armored) ArgumentException.ThrowIfNullOrWhiteSpace(armored); var key = armored.Contains(SecretHeader, StringComparison.Ordinal) - ? PgpKeyFactory.FromArmored(ExportPublic(armored), armored) - : PgpKeyFactory.FromArmored(armored, null); + ? PgpKeyFactory.FromArmored(ExportPublic(armored), armored) + : PgpKeyFactory.FromArmored(armored, null); _byKeyId[key.KeyId] = key; @@ -67,9 +67,7 @@ public bool Remove(string identityOrKeyIdOrFingerprint) /// public bool Contains(string identityOrKeyIdOrFingerprint) - { - return Find(identityOrKeyIdOrFingerprint) is not null; - } + => Find(identityOrKeyIdOrFingerprint) is not null; /// public async Task LoadAsync(IPgpKeyStore store, CancellationToken cancellationToken = default) @@ -95,15 +93,14 @@ public Task SaveAsync(IPgpKeyStore store, CancellationToken cancellationToken = private static string ExportPublic(string secretArmored) { - using var input = PgpUtilities.GetDecoderStream( - new MemoryStream(Encoding.UTF8.GetBytes(secretArmored)) - ); - var ring = new PgpSecretKeyRingBundle(input).GetKeyRings().Cast().First(); + using var input = PgpUtilities.GetDecoderStream(new MemoryStream(Encoding.UTF8.GetBytes(secretArmored))); + var ring = new PgpSecretKeyRingBundle(input).GetKeyRings().First(); using var output = new MemoryStream(); + using (var armor = new ArmoredOutputStream(output)) { - foreach (PgpSecretKey secretKey in ring.GetSecretKeys()) + foreach (var secretKey in ring.GetSecretKeys()) { secretKey.PublicKey.Encode(armor); } diff --git a/src/SquidStd.Crypto/Pgp/Services/PgpService.cs b/src/SquidStd.Crypto/Pgp/Services/PgpService.cs index 6838551f..5ff2d6ff 100644 --- a/src/SquidStd.Crypto/Pgp/Services/PgpService.cs +++ b/src/SquidStd.Crypto/Pgp/Services/PgpService.cs @@ -9,8 +9,8 @@ namespace SquidStd.Crypto.Pgp.Services; /// -/// OpenPGP operations over a keyring, implemented with PgpCore. Every byte/armored-string operation -/// round-trips through so binary payloads survive intact. +/// OpenPGP operations over a keyring, implemented with PgpCore. Every byte/armored-string operation +/// round-trips through so binary payloads survive intact. /// public sealed class PgpService : IPgpService { @@ -51,7 +51,9 @@ public PgpKey GenerateKey(string identity, string passphrase, PgpKeyOptions? opt /// public async Task EncryptForAsync( - string recipientIdentity, byte[] data, CancellationToken cancellationToken = default + string recipientIdentity, + byte[] data, + CancellationToken cancellationToken = default ) { ArgumentNullException.ThrowIfNull(data); @@ -68,7 +70,10 @@ public async Task EncryptForAsync( /// public async Task EncryptForAsync( - string recipientIdentity, Stream input, Stream output, CancellationToken cancellationToken = default + string recipientIdentity, + Stream input, + Stream output, + CancellationToken cancellationToken = default ) { ArgumentNullException.ThrowIfNull(input); @@ -96,7 +101,10 @@ public async Task DecryptAsync(string armored, string passphrase, Cancel /// public async Task DecryptAsync( - Stream input, Stream output, string passphrase, CancellationToken cancellationToken = default + Stream input, + Stream output, + string passphrase, + CancellationToken cancellationToken = default ) { ArgumentNullException.ThrowIfNull(input); @@ -116,7 +124,10 @@ public async Task DecryptAsync( /// public async Task EncryptAndSignForAsync( - string recipientIdentity, byte[] data, string signerIdentity, string signerPassphrase, + string recipientIdentity, + byte[] data, + string signerIdentity, + string signerPassphrase, CancellationToken cancellationToken = default ) { @@ -133,7 +144,11 @@ public async Task EncryptAndSignForAsync( /// public async Task EncryptAndSignForAsync( - string recipientIdentity, Stream input, Stream output, string signerIdentity, string signerPassphrase, + string recipientIdentity, + Stream input, + Stream output, + string signerIdentity, + string signerPassphrase, CancellationToken cancellationToken = default ) { @@ -146,7 +161,9 @@ public async Task EncryptAndSignForAsync( /// public async Task DecryptAndVerifyAsync( - string armored, string passphrase, CancellationToken cancellationToken = default + string armored, + string passphrase, + CancellationToken cancellationToken = default ) { ArgumentException.ThrowIfNullOrWhiteSpace(armored); @@ -161,7 +178,7 @@ public async Task DecryptAndVerifyAsync( { var plain = await DecryptWith(pgp, armored).ConfigureAwait(false); - return new PgpDecryptionResult(plain, false, false); + return new(plain, false, false); } try @@ -170,19 +187,22 @@ public async Task DecryptAndVerifyAsync( using var output = new MemoryStream(); await pgp.DecryptAndVerifyAsync(input, output).ConfigureAwait(false); - return new PgpDecryptionResult(output.ToArray(), true, true); + return new(output.ToArray(), true, true); } catch (Exception ex) when (ex is BcPgpException or InvalidOperationException or ArgumentException or IOException) { var plain = await DecryptWith(pgp, armored).ConfigureAwait(false); - return new PgpDecryptionResult(plain, true, false); + return new(plain, true, false); } } /// public async Task SignAsync( - byte[] data, string signerIdentity, string passphrase, CancellationToken cancellationToken = default + byte[] data, + string signerIdentity, + string passphrase, + CancellationToken cancellationToken = default ) { ArgumentNullException.ThrowIfNull(data); @@ -211,6 +231,7 @@ public async Task VerifyAsync(string signedMessage, Cance var pgp = new PGP(new EncryptionKeys(key.PublicArmored)); bool ok; + try { ok = await pgp.VerifyAsync(input, output).ConfigureAwait(false); @@ -227,11 +248,11 @@ public async Task VerifyAsync(string signedMessage, Cance if (ok) { - return new PgpVerificationResult(true, recovered); + return new(true, recovered); } } - return new PgpVerificationResult(false, recovered); + return new(false, recovered); } private PGP BuildEncryptAndSign(string recipientIdentity, string signerIdentity, string signerPassphrase) @@ -239,7 +260,7 @@ private PGP BuildEncryptAndSign(string recipientIdentity, string signerIdentity, var recipient = RequireKey(recipientIdentity); var signer = RequireKey(signerIdentity); - return new PGP(new EncryptionKeys(recipient.PublicArmored, signer.PrivateArmored!, signerPassphrase)); + return new(new EncryptionKeys(recipient.PublicArmored, signer.PrivateArmored!, signerPassphrase)); } private static async Task DecryptWith(PGP pgp, string armored) @@ -255,8 +276,8 @@ private PgpKey RequireKey(string identity) { ArgumentException.ThrowIfNullOrWhiteSpace(identity); - return _keyring.Find(identity) - ?? throw new KeyNotFoundException($"No key for identity '{identity}' in the keyring."); + return _keyring.Find(identity) ?? + throw new KeyNotFoundException($"No key for identity '{identity}' in the keyring."); } private PgpKey RequireSecretFor(string armored) @@ -275,12 +296,8 @@ private PgpKey RequireSecretFor(string armored) } private static long ParseKeyId(string keyId) - { - return long.Parse(keyId, NumberStyles.HexNumber, CultureInfo.InvariantCulture); - } + => long.Parse(keyId, NumberStyles.HexNumber, CultureInfo.InvariantCulture); private static string ReadAll(MemoryStream stream) - { - return Encoding.UTF8.GetString(stream.ToArray()); - } + => Encoding.UTF8.GetString(stream.ToArray()); } diff --git a/src/SquidStd.Crypto/Pgp/Types/PgpKeyAlgorithm.cs b/src/SquidStd.Crypto/Pgp/Types/PgpKeyAlgorithm.cs index f98a5288..ed52ffa2 100644 --- a/src/SquidStd.Crypto/Pgp/Types/PgpKeyAlgorithm.cs +++ b/src/SquidStd.Crypto/Pgp/Types/PgpKeyAlgorithm.cs @@ -1,7 +1,7 @@ namespace SquidStd.Crypto.Pgp.Types; /// -/// OpenPGP public-key algorithm family for a generated key. Extensible (ECC may be added later). +/// OpenPGP public-key algorithm family for a generated key. Extensible (ECC may be added later). /// public enum PgpKeyAlgorithm { diff --git a/src/SquidStd.Crypto/README.md b/src/SquidStd.Crypto/README.md index 27216f17..cd5d8436 100644 --- a/src/SquidStd.Crypto/README.md +++ b/src/SquidStd.Crypto/README.md @@ -52,14 +52,14 @@ await keyring.LoadAsync(container.Resolve()); ## Key types -| Type | Purpose | -|------|---------| -| `IPgpService` | Key generation, encrypt/decrypt, sign/verify, and combined encrypt+sign / decrypt+verify. | -| `IPgpKeyring` | Stateful, indexed keyring: import keys and save/load via an `IPgpKeyStore`. | -| `IPgpKeyStore` | Pluggable keyring persistence backend. | -| `FilePgpKeyStore` | One armored `.asc` per key (gpg-interoperable). | -| `AesGcmPgpKeyStore` | The whole keyring serialized to a single file, encrypted at rest via `ISecretProtector`. | -| `CryptoFileSystem` | `ILockableFileSystem` that encrypts content and names over any `IVirtualFileSystem`. | +| Type | Purpose | +|---------------------|-------------------------------------------------------------------------------------------| +| `IPgpService` | Key generation, encrypt/decrypt, sign/verify, and combined encrypt+sign / decrypt+verify. | +| `IPgpKeyring` | Stateful, indexed keyring: import keys and save/load via an `IPgpKeyStore`. | +| `IPgpKeyStore` | Pluggable keyring persistence backend. | +| `FilePgpKeyStore` | One armored `.asc` per key (gpg-interoperable). | +| `AesGcmPgpKeyStore` | The whole keyring serialized to a single file, encrypted at rest via `ISecretProtector`. | +| `CryptoFileSystem` | `ILockableFileSystem` that encrypts content and names over any `IVirtualFileSystem`. | ## Key stores diff --git a/src/SquidStd.Crypto/Vfs/Extensions/RegisterCryptoVaultExtensions.cs b/src/SquidStd.Crypto/Vfs/Extensions/RegisterCryptoVaultExtensions.cs index d87faadd..df8f116a 100644 --- a/src/SquidStd.Crypto/Vfs/Extensions/RegisterCryptoVaultExtensions.cs +++ b/src/SquidStd.Crypto/Vfs/Extensions/RegisterCryptoVaultExtensions.cs @@ -13,8 +13,8 @@ public static class RegisterCryptoVaultExtensions extension(IContainer container) { /// - /// Registers an singleton: a crypto vault over a single-file zip at - /// . The consumer calls at runtime. + /// Registers an singleton: a crypto vault over a single-file zip at + /// . The consumer calls at runtime. /// public IContainer RegisterCryptoVault(string path, CryptoVaultOptions? options = null) { diff --git a/src/SquidStd.Crypto/Vfs/Internal/EntryCipher.cs b/src/SquidStd.Crypto/Vfs/Internal/EntryCipher.cs index 03ce8db0..1cda677d 100644 --- a/src/SquidStd.Crypto/Vfs/Internal/EntryCipher.cs +++ b/src/SquidStd.Crypto/Vfs/Internal/EntryCipher.cs @@ -13,7 +13,7 @@ internal sealed class EntryCipher : IDisposable public EntryCipher(byte[] key, int chunkSize) { - _aes = new AesGcm(key, TagSize); + _aes = new(key, TagSize); _chunkSize = chunkSize; } @@ -58,6 +58,7 @@ public async Task DecryptAsync(Stream input, Stream output, CancellationToken ca if (length == 0) { _aes.Decrypt(nonce.Span, ReadOnlySpan.Empty, tag, Span.Empty); + break; } @@ -120,7 +121,5 @@ private static async Task ReadExactAsync(Stream stream, byte[] buffer, Cancellat } public void Dispose() - { - _aes.Dispose(); - } + => _aes.Dispose(); } diff --git a/src/SquidStd.Crypto/Vfs/Internal/VaultHeader.cs b/src/SquidStd.Crypto/Vfs/Internal/VaultHeader.cs index 289a9867..884939ce 100644 --- a/src/SquidStd.Crypto/Vfs/Internal/VaultHeader.cs +++ b/src/SquidStd.Crypto/Vfs/Internal/VaultHeader.cs @@ -14,13 +14,9 @@ int ChunkSize ) { public byte[] Serialize() - { - return JsonSerializer.SerializeToUtf8Bytes(this); - } + => JsonSerializer.SerializeToUtf8Bytes(this); public static VaultHeader Parse(byte[] data) - { - return JsonSerializer.Deserialize(data) - ?? throw new InvalidDataException("Vault header is empty or invalid."); - } + => JsonSerializer.Deserialize(data) ?? + throw new InvalidDataException("Vault header is empty or invalid."); } diff --git a/src/SquidStd.Crypto/Vfs/Internal/VaultIndex.cs b/src/SquidStd.Crypto/Vfs/Internal/VaultIndex.cs index fcd2c56e..cb6c6905 100644 --- a/src/SquidStd.Crypto/Vfs/Internal/VaultIndex.cs +++ b/src/SquidStd.Crypto/Vfs/Internal/VaultIndex.cs @@ -25,7 +25,7 @@ public IReadOnlyDictionary Entries public VaultIndex() { - _entries = new Dictionary(StringComparer.Ordinal); + _entries = new(StringComparer.Ordinal); } private VaultIndex(Dictionary entries) @@ -67,9 +67,9 @@ public byte[] Serialize() public static VaultIndex Parse(byte[] data) { - var map = JsonSerializer.Deserialize>(data) - ?? new Dictionary(StringComparer.Ordinal); + var map = JsonSerializer.Deserialize>(data) ?? + new Dictionary(StringComparer.Ordinal); - return new VaultIndex(new Dictionary(map, StringComparer.Ordinal)); + return new(new(map, StringComparer.Ordinal)); } } diff --git a/src/SquidStd.Crypto/Vfs/Internal/VaultKeyDerivation.cs b/src/SquidStd.Crypto/Vfs/Internal/VaultKeyDerivation.cs index abb5212a..9be8c299 100644 --- a/src/SquidStd.Crypto/Vfs/Internal/VaultKeyDerivation.cs +++ b/src/SquidStd.Crypto/Vfs/Internal/VaultKeyDerivation.cs @@ -12,12 +12,12 @@ internal static class VaultKeyDerivation public static byte[] DeriveMasterKey(string passphrase, byte[] salt, CryptoVaultOptions options) { var parameters = new Argon2Parameters.Builder(Argon2Parameters.Argon2id) - .WithVersion(Argon2Parameters.Version13) - .WithSalt(salt) - .WithIterations(options.Argon2Iterations) - .WithMemoryAsKB(options.Argon2MemoryKib) - .WithParallelism(options.Argon2Parallelism) - .Build(); + .WithVersion(Argon2Parameters.Version13) + .WithSalt(salt) + .WithIterations(options.Argon2Iterations) + .WithMemoryAsKB(options.Argon2MemoryKib) + .WithParallelism(options.Argon2Parallelism) + .Build(); var generator = new Argon2BytesGenerator(); generator.Init(parameters); diff --git a/src/SquidStd.Crypto/Vfs/Services/CryptoFileSystem.cs b/src/SquidStd.Crypto/Vfs/Services/CryptoFileSystem.cs index 078fca26..920ecfd1 100644 --- a/src/SquidStd.Crypto/Vfs/Services/CryptoFileSystem.cs +++ b/src/SquidStd.Crypto/Vfs/Services/CryptoFileSystem.cs @@ -2,9 +2,9 @@ using System.Security.Cryptography; using SquidStd.Crypto.Vfs.Data; using SquidStd.Crypto.Vfs.Internal; +using SquidStd.Vfs.Abstractions; using SquidStd.Vfs.Abstractions.Data; using SquidStd.Vfs.Abstractions.Interfaces; -using SquidStd.Vfs.Abstractions; namespace SquidStd.Crypto.Vfs.Services; @@ -35,7 +35,7 @@ public void Unlock(string passphrase) if (headerBytes is null) { - header = new VaultHeader( + header = new( "SQVFS1", 1, RandomNumberGenerator.GetBytes(16), @@ -62,8 +62,8 @@ public void Unlock(string passphrase) var indexBytes = _inner.ReadAllBytesAsync(IndexPath).AsTask().GetAwaiter().GetResult(); _index = indexBytes is null - ? new VaultIndex() - : VaultIndex.Parse(VaultBlob.Decrypt(VaultKeyDerivation.DeriveSubKey(master, "index"), indexBytes)); + ? new() + : VaultIndex.Parse(VaultBlob.Decrypt(VaultKeyDerivation.DeriveSubKey(master, "index"), indexBytes)); _masterKey = master; } @@ -98,8 +98,8 @@ public ValueTask ExistsAsync(string path, CancellationToken cancellationTo return null; } - var blob = await _inner.ReadAllBytesAsync(entry!.BlobId, cancellationToken).ConfigureAwait(false) - ?? throw new InvalidDataException($"Backing blob '{entry.BlobId}' is missing."); + var blob = await _inner.ReadAllBytesAsync(entry!.BlobId, cancellationToken).ConfigureAwait(false) ?? + throw new InvalidDataException($"Backing blob '{entry.BlobId}' is missing."); using var cipher = NewCipher(entry.BlobId); using var input = new MemoryStream(blob); @@ -110,7 +110,9 @@ public ValueTask ExistsAsync(string path, CancellationToken cancellationTo } public async ValueTask WriteAllBytesAsync( - string path, ReadOnlyMemory data, CancellationToken cancellationToken = default + string path, + ReadOnlyMemory data, + CancellationToken cancellationToken = default ) { EnsureUnlocked(); @@ -124,15 +126,15 @@ public async ValueTask WriteAllBytesAsync( await cipher.EncryptAsync(input, output, cancellationToken).ConfigureAwait(false); await _inner.WriteAllBytesAsync(blobId, output.ToArray(), cancellationToken).ConfigureAwait(false); - _index.Set(normalized, new VaultIndexEntry(blobId, data.Length, DateTimeOffset.UtcNow)); + _index.Set(normalized, new(blobId, data.Length, DateTimeOffset.UtcNow)); } public async Task OpenReadAsync(string path, CancellationToken cancellationToken = default) { - var data = await ReadAllBytesAsync(path, cancellationToken).ConfigureAwait(false) - ?? throw new FileNotFoundException($"No file at '{path}'.", path); + var data = await ReadAllBytesAsync(path, cancellationToken).ConfigureAwait(false) ?? + throw new FileNotFoundException($"No file at '{path}'.", path); - return new MemoryStream(data, writable: false); + return new MemoryStream(data, false); } public Task OpenWriteAsync(string path, CancellationToken cancellationToken = default) @@ -157,7 +159,8 @@ public async ValueTask DeleteAsync(string path, CancellationToken cancella } public async IAsyncEnumerable ListAsync( - string? prefix = null, [EnumeratorCancellation] CancellationToken cancellationToken = default + string? prefix = null, + [EnumeratorCancellation] CancellationToken cancellationToken = default ) { EnsureUnlocked(); @@ -170,21 +173,17 @@ public async IAsyncEnumerable ListAsync( continue; } - yield return new VfsEntry(logicalPath, entry.Size, entry.ModifiedUtc); + yield return new(logicalPath, entry.Size, entry.ModifiedUtc); await Task.CompletedTask; } } private EntryCipher NewCipher(string blobId) - { - return new EntryCipher(VaultKeyDerivation.DeriveSubKey(_masterKey!, "entry:" + blobId), _options.ChunkSize); - } + => new(VaultKeyDerivation.DeriveSubKey(_masterKey!, "entry:" + blobId), _options.ChunkSize); private static string NewBlobId() - { - return Convert.ToHexStringLower(RandomNumberGenerator.GetBytes(16)); - } + => Convert.ToHexStringLower(RandomNumberGenerator.GetBytes(16)); private void FlushIndex() { @@ -203,6 +202,7 @@ private void PruneOrphans() var paths = new List(); var enumerator = _inner.ListAsync().GetAsyncEnumerator(); + try { while (enumerator.MoveNextAsync().AsTask().GetAwaiter().GetResult()) @@ -239,9 +239,11 @@ public void Dispose() { case IDisposable disposable: disposable.Dispose(); + break; case IAsyncDisposable asyncDisposable: asyncDisposable.DisposeAsync().AsTask().GetAwaiter().GetResult(); + break; } } diff --git a/src/SquidStd.Database.Abstractions/Data/Database/DatabaseConfig.cs b/src/SquidStd.Database.Abstractions/Data/Database/DatabaseConfig.cs index d386fd1b..39fdc203 100644 --- a/src/SquidStd.Database.Abstractions/Data/Database/DatabaseConfig.cs +++ b/src/SquidStd.Database.Abstractions/Data/Database/DatabaseConfig.cs @@ -3,18 +3,18 @@ namespace SquidStd.Database.Abstractions.Data.Database; /// -/// Database connection configuration. +/// Database connection configuration. /// public sealed class DatabaseConfig : IConfigEntry { /// - /// Gets or sets the URI-style connection string (e.g. "sqlite://squidstd.db", - /// "postgres://user:pass@host:5432/db"). The scheme selects the provider. + /// Gets or sets the URI-style connection string (e.g. "sqlite://squidstd.db", + /// "postgres://user:pass@host:5432/db"). The scheme selects the provider. /// public string ConnectionString { get; set; } = "sqlite://squidstd.db"; /// - /// Gets or sets a value indicating whether the schema is auto-synchronized on startup. + /// Gets or sets a value indicating whether the schema is auto-synchronized on startup. /// public bool AutoMigrate { get; set; } = true; @@ -23,7 +23,5 @@ public sealed class DatabaseConfig : IConfigEntry Type IConfigEntry.ConfigType => typeof(DatabaseConfig); object IConfigEntry.CreateDefault() - { - return new DatabaseConfig(); - } + => new DatabaseConfig(); } diff --git a/src/SquidStd.Database.Abstractions/Data/Entities/BaseEntity.cs b/src/SquidStd.Database.Abstractions/Data/Entities/BaseEntity.cs index 85dc2da7..aee115e9 100644 --- a/src/SquidStd.Database.Abstractions/Data/Entities/BaseEntity.cs +++ b/src/SquidStd.Database.Abstractions/Data/Entities/BaseEntity.cs @@ -1,7 +1,7 @@ namespace SquidStd.Database.Abstractions.Data.Entities; /// -/// Base class for all persisted entities: a Guid identity plus UTC create/update timestamps. +/// Base class for all persisted entities: a Guid identity plus UTC create/update timestamps. /// public abstract class BaseEntity { diff --git a/src/SquidStd.Database.Abstractions/Data/PagedResultData.cs b/src/SquidStd.Database.Abstractions/Data/PagedResultData.cs index cd16c889..a259ad19 100644 --- a/src/SquidStd.Database.Abstractions/Data/PagedResultData.cs +++ b/src/SquidStd.Database.Abstractions/Data/PagedResultData.cs @@ -1,7 +1,7 @@ namespace SquidStd.Database.Abstractions.Data; /// -/// A paginated result set with paging metadata. +/// A paginated result set with paging metadata. /// /// The item type. public sealed class PagedResultData @@ -28,7 +28,7 @@ public sealed class PagedResultData public bool HasPrevious => Page > 1 && TotalPages > 0; /// - /// Creates a paged result. + /// Creates a paged result. /// /// The current page items. /// The 1-based page number. @@ -36,13 +36,11 @@ public sealed class PagedResultData /// The total matching row count. /// The paged result. public static PagedResultData Create(IReadOnlyList items, int page, int pageSize, long totalCount) - { - return new PagedResultData + => new() { Items = items, Page = page, PageSize = pageSize, TotalCount = totalCount }; - } } diff --git a/src/SquidStd.Database.Abstractions/Interfaces/Data/IDataAccess.cs b/src/SquidStd.Database.Abstractions/Interfaces/Data/IDataAccess.cs index 8ba1f9ee..a5d8e020 100644 --- a/src/SquidStd.Database.Abstractions/Interfaces/Data/IDataAccess.cs +++ b/src/SquidStd.Database.Abstractions/Interfaces/Data/IDataAccess.cs @@ -6,7 +6,7 @@ namespace SquidStd.Database.Abstractions.Interfaces.Data; /// -/// Generic data access for a type: CRUD, bulk, and querying. +/// Generic data access for a type: CRUD, bulk, and querying. /// /// The entity type. public interface IDataAccess diff --git a/src/SquidStd.Database.Abstractions/Types/Data/DatabaseProviderType.cs b/src/SquidStd.Database.Abstractions/Types/Data/DatabaseProviderType.cs index 530fe8ed..81955055 100644 --- a/src/SquidStd.Database.Abstractions/Types/Data/DatabaseProviderType.cs +++ b/src/SquidStd.Database.Abstractions/Types/Data/DatabaseProviderType.cs @@ -1,7 +1,7 @@ namespace SquidStd.Database.Abstractions.Types.Data; /// -/// Supported database providers. +/// Supported database providers. /// public enum DatabaseProviderType { diff --git a/src/SquidStd.Database/Connection/ConnectionStringParser.cs b/src/SquidStd.Database/Connection/ConnectionStringParser.cs index 2eb883cc..1559f0c0 100644 --- a/src/SquidStd.Database/Connection/ConnectionStringParser.cs +++ b/src/SquidStd.Database/Connection/ConnectionStringParser.cs @@ -4,12 +4,12 @@ namespace SquidStd.Database.Connection; /// -/// Parses URI-style connection strings ("scheme://...") into a provider and native connection string. +/// Parses URI-style connection strings ("scheme://...") into a provider and native connection string. /// public static class ConnectionStringParser { /// - /// Parses the given URI connection string. + /// Parses the given URI connection string. /// /// The URI connection string. /// The parsed provider and native connection string. @@ -29,10 +29,10 @@ public static ParsedConnection Parse(string connectionString) var provider = ResolveProvider(scheme); var native = provider == DatabaseProviderType.Sqlite - ? BuildSqlite(remainder) - : BuildServer(provider, remainder); + ? BuildSqlite(remainder) + : BuildServer(provider, remainder); - return new ParsedConnection(provider, native); + return new(provider, native); } private static string BuildServer(DatabaseProviderType provider, string remainder) @@ -87,8 +87,7 @@ private static string BuildSqlite(string remainder) } private static DatabaseProviderType ResolveProvider(string scheme) - { - return scheme switch + => scheme switch { "sqlite" => DatabaseProviderType.Sqlite, "postgres" or "postgresql" => DatabaseProviderType.Postgres, @@ -96,7 +95,6 @@ private static DatabaseProviderType ResolveProvider(string scheme) "mysql" => DatabaseProviderType.MySql, _ => throw new NotSupportedException($"Unsupported database scheme '{scheme}'.") }; - } private static (string User, string Password, string HostPort) SplitAuthority(string authority) { diff --git a/src/SquidStd.Database/Connection/ParsedConnection.cs b/src/SquidStd.Database/Connection/ParsedConnection.cs index 6603c3fd..b03c0d84 100644 --- a/src/SquidStd.Database/Connection/ParsedConnection.cs +++ b/src/SquidStd.Database/Connection/ParsedConnection.cs @@ -3,7 +3,7 @@ namespace SquidStd.Database.Connection; /// -/// The result of parsing a URI connection string: the provider and the native connection string. +/// The result of parsing a URI connection string: the provider and the native connection string. /// /// The resolved database provider. /// The provider-native connection string for FreeSql. diff --git a/src/SquidStd.Database/Data/FreeSqlDataAccess.cs b/src/SquidStd.Database/Data/FreeSqlDataAccess.cs index 65a9dec0..b1fe240e 100644 --- a/src/SquidStd.Database/Data/FreeSqlDataAccess.cs +++ b/src/SquidStd.Database/Data/FreeSqlDataAccess.cs @@ -10,7 +10,7 @@ namespace SquidStd.Database.Data; /// -/// FreeSql-backed . Writes run inside a unit of work with rollback. +/// FreeSql-backed . Writes run inside a unit of work with rollback. /// /// The entity type. public sealed class FreeSqlDataAccess : IDataAccess @@ -21,7 +21,7 @@ public sealed class FreeSqlDataAccess : IDataAccess private readonly IFreeSql _orm; /// - /// Initializes the data access over the shared FreeSql instance. + /// Initializes the data access over the shared FreeSql instance. /// /// The database service that owns the FreeSql instance. public FreeSqlDataAccess(IDatabaseService databaseService) @@ -34,17 +34,15 @@ public Task BulkDeleteAsync( Expression> predicate, CancellationToken cancellationToken = default ) - { - return RunInTransactionAsync( + => RunInTransactionAsync( transaction => _orm.Delete() - .Where(predicate) - .WithTransaction(transaction) - .ExecuteAffrowsAsync(cancellationToken), + .Where(predicate) + .WithTransaction(transaction) + .ExecuteAffrowsAsync(cancellationToken), "BulkDelete", null, cancellationToken ); - } /// public Task BulkInsertAsync(IEnumerable entities, CancellationToken cancellationToken = default) @@ -72,9 +70,9 @@ public Task BulkUpdateAsync(IEnumerable entities, CancellationToke return RunInTransactionAsync( transaction => _orm.Update() - .SetSource(list) - .WithTransaction(transaction) - .ExecuteAffrowsAsync(cancellationToken), + .SetSource(list) + .WithTransaction(transaction) + .ExecuteAffrowsAsync(cancellationToken), "BulkUpdate", list.Count, cancellationToken @@ -101,35 +99,29 @@ public Task CountAsync( public async Task DeleteAsync(Guid id, CancellationToken cancellationToken = default) { var affected = await RunInTransactionAsync( - transaction => _orm.Delete() - .Where(e => e.Id == id) - .WithTransaction(transaction) - .ExecuteAffrowsAsync(cancellationToken), - "Delete", - null, - cancellationToken - ); + transaction => _orm.Delete() + .Where(e => e.Id == id) + .WithTransaction(transaction) + .ExecuteAffrowsAsync(cancellationToken), + "Delete", + null, + cancellationToken + ); return affected > 0; } /// public Task DeleteAsync(TEntity entity, CancellationToken cancellationToken = default) - { - return DeleteAsync(entity.Id, cancellationToken); - } + => DeleteAsync(entity.Id, cancellationToken); /// public Task ExistsAsync(Expression> predicate, CancellationToken cancellationToken = default) - { - return _orm.Select().Where(predicate).AnyAsync(cancellationToken); - } + => _orm.Select().Where(predicate).AnyAsync(cancellationToken); /// public Task GetByIdAsync(Guid id, CancellationToken cancellationToken = default) - { - return _orm.Select().Where(e => e.Id == id).FirstAsync(cancellationToken)!; - } + => _orm.Select().Where(e => e.Id == id).FirstAsync(cancellationToken)!; /// public async Task> GetPagedAsync( @@ -185,9 +177,7 @@ await RunInTransactionAsync( /// public ISelect Query() - { - return _orm.Select(); - } + => _orm.Select(); /// public async Task> QueryAsync( @@ -212,9 +202,9 @@ public async Task UpdateAsync(TEntity entity, CancellationToken cancell await RunInTransactionAsync( transaction => _orm.Update() - .SetSource(entity) - .WithTransaction(transaction) - .ExecuteAffrowsAsync(cancellationToken), + .SetSource(entity) + .WithTransaction(transaction) + .ExecuteAffrowsAsync(cancellationToken), "Update", 1, cancellationToken diff --git a/src/SquidStd.Database/Extensions/RegisterDatabaseExtension.cs b/src/SquidStd.Database/Extensions/RegisterDatabaseExtension.cs index abb91982..a357e873 100644 --- a/src/SquidStd.Database/Extensions/RegisterDatabaseExtension.cs +++ b/src/SquidStd.Database/Extensions/RegisterDatabaseExtension.cs @@ -10,7 +10,7 @@ namespace SquidStd.Database.Extensions; /// -/// DI registration for the database subsystem. +/// DI registration for the database subsystem. /// public static class RegisterDatabaseExtension { @@ -18,7 +18,7 @@ public static class RegisterDatabaseExtension extension(IContainer container) { /// - /// Registers the database config section, the database service, and the open-generic data access. + /// Registers the database config section, the database service, and the open-generic data access. /// /// The same container for chaining. public IContainer RegisterDatabase() diff --git a/src/SquidStd.Database/Extensions/ZLinqResultExtensions.cs b/src/SquidStd.Database/Extensions/ZLinqResultExtensions.cs index 16922587..5985a19a 100644 --- a/src/SquidStd.Database/Extensions/ZLinqResultExtensions.cs +++ b/src/SquidStd.Database/Extensions/ZLinqResultExtensions.cs @@ -3,7 +3,7 @@ namespace SquidStd.Database.Extensions; /// -/// Zero-allocation, in-memory helpers (ZLinq) over already-materialized result lists. +/// Zero-allocation, in-memory helpers (ZLinq) over already-materialized result lists. /// public static class ZLinqResultExtensions { @@ -11,25 +11,21 @@ public static class ZLinqResultExtensions extension(IReadOnlyList source) { /// - /// Projects each materialized item to a new form using ZLinq, returning a list. + /// Projects each materialized item to a new form using ZLinq, returning a list. /// /// The source item type. /// The projected item type. /// The projection. /// The projected list. - public List MapToList( - Func selector - ) - { - return source.AsValueEnumerable().Select(selector).ToList(); - } + public List MapToList(Func selector) + => source.AsValueEnumerable().Select(selector).ToList(); } /// The materialized source items. extension(IReadOnlyList source) { /// - /// Takes an in-memory page of a materialized list using ZLinq (no SQL involved). + /// Takes an in-memory page of a materialized list using ZLinq (no SQL involved). /// /// The item type. /// The 1-based page number. diff --git a/src/SquidStd.Database/Interfaces/Services/IDatabaseService.cs b/src/SquidStd.Database/Interfaces/Services/IDatabaseService.cs index 1662dde7..0ce04e6d 100644 --- a/src/SquidStd.Database/Interfaces/Services/IDatabaseService.cs +++ b/src/SquidStd.Database/Interfaces/Services/IDatabaseService.cs @@ -3,7 +3,7 @@ namespace SquidStd.Database.Interfaces.Services; /// -/// Owns the application's singleton FreeSql instance and its lifecycle. +/// Owns the application's singleton FreeSql instance and its lifecycle. /// public interface IDatabaseService : ISquidStdService { diff --git a/src/SquidStd.Database/Services/DatabaseService.cs b/src/SquidStd.Database/Services/DatabaseService.cs index e9a7512c..7b606ea2 100644 --- a/src/SquidStd.Database/Services/DatabaseService.cs +++ b/src/SquidStd.Database/Services/DatabaseService.cs @@ -8,7 +8,7 @@ namespace SquidStd.Database.Services; /// -/// Builds and owns the singleton FreeSql instance, logging SQL and migrations verbosely. +/// Builds and owns the singleton FreeSql instance, logging SQL and migrations verbosely. /// public sealed class DatabaseService : IDatabaseService { @@ -22,7 +22,7 @@ public sealed class DatabaseService : IDatabaseService public IFreeSql Orm => _orm ?? throw new InvalidOperationException("Database service is not started."); /// - /// Initializes the database service. + /// Initializes the database service. /// /// The database configuration section. public DatabaseService(DatabaseConfig config) @@ -44,13 +44,13 @@ public ValueTask StartAsync(CancellationToken cancellationToken = default) Logger.Verbose("Building FreeSql for provider {Provider}", parsed.Provider); var builder = new FreeSqlBuilder() - .UseConnectionString(MapDataType(parsed.Provider), parsed.NativeConnectionString) - .UseAutoSyncStructure(_config.AutoMigrate) - .UseMonitorCommand(cmd => Logger.Verbose("SQL {Sql}", cmd.CommandText)); + .UseConnectionString(MapDataType(parsed.Provider), parsed.NativeConnectionString) + .UseAutoSyncStructure(_config.AutoMigrate) + .UseMonitorCommand(cmd => Logger.Verbose("SQL {Sql}", cmd.CommandText)); _orm = builder.Build(); _orm.Aop.SyncStructureAfter += (_, e) => - Logger.Verbose("Migrated {Entities} -> {Sql}", e.EntityTypes, e.Sql); + Logger.Verbose("Migrated {Entities} -> {Sql}", e.EntityTypes, e.Sql); Logger.Information( "Database service started ({Provider}, autoMigrate={AutoMigrate})", @@ -73,8 +73,7 @@ public ValueTask StopAsync(CancellationToken cancellationToken = default) } private static DataType MapDataType(DatabaseProviderType provider) - { - return provider switch + => provider switch { DatabaseProviderType.Sqlite => DataType.Sqlite, DatabaseProviderType.Postgres => DataType.PostgreSQL, @@ -82,5 +81,4 @@ private static DataType MapDataType(DatabaseProviderType provider) DatabaseProviderType.MySql => DataType.MySql, _ => throw new NotSupportedException($"Unsupported provider {provider}.") }; - } } diff --git a/src/SquidStd.Generators/AnalyzerReleases.Unshipped.md b/src/SquidStd.Generators/AnalyzerReleases.Unshipped.md index 8dd9d9fb..822e1d6d 100644 --- a/src/SquidStd.Generators/AnalyzerReleases.Unshipped.md +++ b/src/SquidStd.Generators/AnalyzerReleases.Unshipped.md @@ -3,10 +3,10 @@ ### New Rules -Rule ID | Category | Severity | Notes ---------|----------|----------|------ -SQDGEN001 | SquidStd.Generators | Warning | Event listener cannot be generated -SQDGEN002 | SquidStd.Generators | Warning | Standard service cannot be generated -SQDGEN003 | SquidStd.Generators | Warning | Config section cannot be generated -SQDGEN004 | SquidStd.Generators | Warning | Job handler cannot be generated -SQDGEN005 | SquidStd.Generators | Warning | Script module cannot be generated + Rule ID | Category | Severity | Notes +-----------|---------------------|----------|-------------------------------------- + SQDGEN001 | SquidStd.Generators | Warning | Event listener cannot be generated + SQDGEN002 | SquidStd.Generators | Warning | Standard service cannot be generated + SQDGEN003 | SquidStd.Generators | Warning | Config section cannot be generated + SQDGEN004 | SquidStd.Generators | Warning | Job handler cannot be generated + SQDGEN005 | SquidStd.Generators | Warning | Script module cannot be generated diff --git a/src/SquidStd.Generators/Common/GeneratorSymbolHelpers.cs b/src/SquidStd.Generators/Common/GeneratorSymbolHelpers.cs index 6f10a228..10108900 100644 --- a/src/SquidStd.Generators/Common/GeneratorSymbolHelpers.cs +++ b/src/SquidStd.Generators/Common/GeneratorSymbolHelpers.cs @@ -5,24 +5,16 @@ namespace SquidStd.Generators.Common; internal static class GeneratorSymbolHelpers { public static string FullyQualified(ITypeSymbol symbol) - { - return symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); - } + => symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); public static string DisplayName(ISymbol symbol) - { - return symbol.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat); - } + => symbol.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat); public static Location? PrimaryLocation(ISymbol symbol) - { - return symbol.Locations.FirstOrDefault(); - } + => symbol.Locations.FirstOrDefault(); public static bool IsConcreteNonGenericClass(INamedTypeSymbol type) - { - return type.TypeKind == TypeKind.Class && !type.IsAbstract && !type.IsGenericType; - } + => type.TypeKind == TypeKind.Class && !type.IsAbstract && !type.IsGenericType; public static bool IsAccessibleFromGeneratedSource(INamedTypeSymbol type) { @@ -42,16 +34,15 @@ public static bool IsAccessibleFromGeneratedSource(INamedTypeSymbol type) } public static bool ImplementsInterface(INamedTypeSymbol type, string metadataName, string namespaceName) - { - return type.AllInterfaces.Any(interfaceType => + => type.AllInterfaces.Any( + interfaceType => { var originalDefinition = interfaceType.OriginalDefinition; - return originalDefinition.MetadataName == metadataName - && originalDefinition.ContainingNamespace.ToDisplayString() == namespaceName; + return originalDefinition.MetadataName == metadataName && + originalDefinition.ContainingNamespace.ToDisplayString() == namespaceName; } ); - } public static bool IsAssignableTo(INamedTypeSymbol implementationType, INamedTypeSymbol serviceType) { @@ -68,15 +59,15 @@ public static bool IsAssignableTo(INamedTypeSymbol implementationType, INamedTyp } } - return implementationType.AllInterfaces.Any(interfaceType => - SymbolEqualityComparer.Default.Equals(interfaceType, serviceType) + return implementationType.AllInterfaces.Any( + interfaceType => + SymbolEqualityComparer.Default.Equals(interfaceType, serviceType) ); } public static bool HasPublicParameterlessConstructor(INamedTypeSymbol type) - { - return type.InstanceConstructors.Any(constructor => - constructor.Parameters.Length == 0 && constructor.DeclaredAccessibility == Accessibility.Public + => type.InstanceConstructors.Any( + constructor => + constructor.Parameters.Length == 0 && constructor.DeclaredAccessibility == Accessibility.Public ); - } } diff --git a/src/SquidStd.Generators/Config/ConfigSectionRegistrationGenerator.cs b/src/SquidStd.Generators/Config/ConfigSectionRegistrationGenerator.cs index c45894b8..ec82ca29 100644 --- a/src/SquidStd.Generators/Config/ConfigSectionRegistrationGenerator.cs +++ b/src/SquidStd.Generators/Config/ConfigSectionRegistrationGenerator.cs @@ -35,12 +35,12 @@ CancellationToken cancellationToken var sectionName = GetSectionName(attribute); var priority = GetIntNamedArgument(attribute, "Priority"); - var isSupported = !string.IsNullOrWhiteSpace(sectionName) - && GeneratorSymbolHelpers.IsConcreteNonGenericClass(configType) - && GeneratorSymbolHelpers.IsAccessibleFromGeneratedSource(configType) - && GeneratorSymbolHelpers.HasPublicParameterlessConstructor(configType); + var isSupported = !string.IsNullOrWhiteSpace(sectionName) && + GeneratorSymbolHelpers.IsConcreteNonGenericClass(configType) && + GeneratorSymbolHelpers.IsAccessibleFromGeneratedSource(configType) && + GeneratorSymbolHelpers.HasPublicParameterlessConstructor(configType); - return new ConfigSectionRegistrationCandidate( + return new( GeneratorSymbolHelpers.FullyQualified(configType), sectionName ?? string.Empty, GeneratorSymbolHelpers.DisplayName(configType), @@ -97,19 +97,21 @@ ImmutableArray candidates } var key = candidate.SectionName + "|" + candidate.ConfigTypeName; + if (seenKeys.Add(key)) { supported.Add(candidate); } } - supported.Sort(static (left, right) => + supported.Sort( + static (left, right) => { var sectionComparison = string.Compare(left.SectionName, right.SectionName, StringComparison.Ordinal); return sectionComparison != 0 - ? sectionComparison - : string.Compare(left.ConfigTypeName, right.ConfigTypeName, StringComparison.Ordinal); + ? sectionComparison + : string.Compare(left.ConfigTypeName, right.ConfigTypeName, StringComparison.Ordinal); } ); diff --git a/src/SquidStd.Generators/Config/ConfigSectionSourceBuilder.cs b/src/SquidStd.Generators/Config/ConfigSectionSourceBuilder.cs index d145ddd8..75db280b 100644 --- a/src/SquidStd.Generators/Config/ConfigSectionSourceBuilder.cs +++ b/src/SquidStd.Generators/Config/ConfigSectionSourceBuilder.cs @@ -43,7 +43,5 @@ public static string Build(IReadOnlyList can } private static string FormatStringLiteral(string value) - { - return "\"" + value.Replace("\\", "\\\\").Replace("\"", "\\\"") + "\""; - } + => "\"" + value.Replace("\\", "\\\\").Replace("\"", "\\\"") + "\""; } diff --git a/src/SquidStd.Generators/Events/EventListenerRegistrationGenerator.cs b/src/SquidStd.Generators/Events/EventListenerRegistrationGenerator.cs index 3556912a..df4b7844 100644 --- a/src/SquidStd.Generators/Events/EventListenerRegistrationGenerator.cs +++ b/src/SquidStd.Generators/Events/EventListenerRegistrationGenerator.cs @@ -51,6 +51,7 @@ CancellationToken cancellationToken for (var i = 0; i < listenerType.AllInterfaces.Length; i++) { var interfaceType = listenerType.AllInterfaces[i]; + if (!IsEventListenerInterface(interfaceType)) { continue; @@ -61,12 +62,12 @@ CancellationToken cancellationToken continue; } - var isSupported = !listenerType.IsGenericType - && IsAccessibleFromGeneratedSource(listenerType) - && IsAccessibleFromGeneratedSource(eventType); + var isSupported = !listenerType.IsGenericType && + IsAccessibleFromGeneratedSource(listenerType) && + IsAccessibleFromGeneratedSource(eventType); candidates.Add( - new EventListenerCandidate( + new( eventType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), listenerType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), listenerType.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat), @@ -83,8 +84,8 @@ private static bool IsEventListenerInterface(INamedTypeSymbol interfaceType) { var originalDefinition = interfaceType.OriginalDefinition; - return originalDefinition.MetadataName == EventListenerMetadataName - && originalDefinition.ContainingNamespace.ToDisplayString() == EventListenerNamespace; + return originalDefinition.MetadataName == EventListenerMetadataName && + originalDefinition.ContainingNamespace.ToDisplayString() == EventListenerNamespace; } private static bool IsAccessibleFromGeneratedSource(INamedTypeSymbol type) @@ -134,6 +135,7 @@ ImmutableArray> candidateGroups } var key = candidate.EventTypeName + "|" + candidate.ListenerTypeName; + if (seenKeys.Add(key)) { candidates.Add(candidate); @@ -141,7 +143,8 @@ ImmutableArray> candidateGroups } } - candidates.Sort(static (left, right) => string.Compare( + candidates.Sort( + static (left, right) => string.Compare( left.ListenerTypeName, right.ListenerTypeName, StringComparison.Ordinal diff --git a/src/SquidStd.Generators/README.md b/src/SquidStd.Generators/README.md index 405eb948..4cf86aa9 100644 --- a/src/SquidStd.Generators/README.md +++ b/src/SquidStd.Generators/README.md @@ -10,7 +10,8 @@ dotnet add package SquidStd.Generators ## Usage -The event listener generator discovers concrete `IEventListener` implementations marked with `[RegisterEventListener]` and generates a DryIoc registration extension: +The event listener generator discovers concrete `IEventListener` implementations marked with +`[RegisterEventListener]` and generates a DryIoc registration extension: ```csharp using SquidStd.Abstractions.Attributes; @@ -26,22 +27,27 @@ public sealed class PingListener : IEventListener container.RegisterGeneratedEventListeners(); ``` -The generated method reuses the normal `RegisterEventListener()` runtime path, so listener activation stays compatible with `SquidStd.Services.Core`. Each registration family has its own marker attribute and generated extension method, all calling the same runtime APIs as manual registration (`RegisterStdService`, `RegisterConfigSection`, `AddJobHandler`, `RegisterScriptModule`). +The generated method reuses the normal `RegisterEventListener()` runtime path, so listener activation stays +compatible with `SquidStd.Services.Core`. Each registration family has its own marker attribute and generated extension +method, all calling the same runtime APIs as manual registration (`RegisterStdService`, `RegisterConfigSection`, +`AddJobHandler`, `RegisterScriptModule`). ## Key types -| Marker attribute | Generated method | -|------------------|------------------| -| `[RegisterEventListener]` | `RegisterGeneratedEventListeners()` | -| `[RegisterStdService(typeof(IMyService), Priority = 10)]` | `RegisterGeneratedStdServices()` | -| `[RegisterConfigSection("workers", Priority = -50)]` | `RegisterGeneratedConfigSections()` | -| `[RegisterJobHandler]` | `RegisterGeneratedJobHandlers()` | -| `[RegisterScriptModule]` with `[ScriptModule("name")]` | `RegisterGeneratedScriptModules()` | +| Marker attribute | Generated method | +|-----------------------------------------------------------|-------------------------------------| +| `[RegisterEventListener]` | `RegisterGeneratedEventListeners()` | +| `[RegisterStdService(typeof(IMyService), Priority = 10)]` | `RegisterGeneratedStdServices()` | +| `[RegisterConfigSection("workers", Priority = -50)]` | `RegisterGeneratedConfigSections()` | +| `[RegisterJobHandler]` | `RegisterGeneratedJobHandlers()` | +| `[RegisterScriptModule]` with `[ScriptModule("name")]` | `RegisterGeneratedScriptModules()` | ## Related -- Tutorial: [Source generators: event listeners](https://tgiachi.github.io/squid-std/tutorials/source-generators-event-listeners.html) -- Tutorial: [Source generators: registration](https://tgiachi.github.io/squid-std/tutorials/source-generators-registration.html) +- +Tutorial: [Source generators: event listeners](https://tgiachi.github.io/squid-std/tutorials/source-generators-event-listeners.html) +- +Tutorial: [Source generators: registration](https://tgiachi.github.io/squid-std/tutorials/source-generators-registration.html) ## License diff --git a/src/SquidStd.Generators/Scripting/Lua/ScriptModuleRegistrationGenerator.cs b/src/SquidStd.Generators/Scripting/Lua/ScriptModuleRegistrationGenerator.cs index 5a4a0427..cafde2d0 100644 --- a/src/SquidStd.Generators/Scripting/Lua/ScriptModuleRegistrationGenerator.cs +++ b/src/SquidStd.Generators/Scripting/Lua/ScriptModuleRegistrationGenerator.cs @@ -33,11 +33,11 @@ CancellationToken cancellationToken cancellationToken.ThrowIfCancellationRequested(); var scriptModuleType = (INamedTypeSymbol)context.TargetSymbol; - var isSupported = GeneratorSymbolHelpers.IsConcreteNonGenericClass(scriptModuleType) - && GeneratorSymbolHelpers.IsAccessibleFromGeneratedSource(scriptModuleType) - && HasScriptModuleAttribute(scriptModuleType); + var isSupported = GeneratorSymbolHelpers.IsConcreteNonGenericClass(scriptModuleType) && + GeneratorSymbolHelpers.IsAccessibleFromGeneratedSource(scriptModuleType) && + HasScriptModuleAttribute(scriptModuleType); - return new ScriptModuleRegistrationCandidate( + return new( GeneratorSymbolHelpers.FullyQualified(scriptModuleType), GeneratorSymbolHelpers.DisplayName(scriptModuleType), GeneratorSymbolHelpers.PrimaryLocation(scriptModuleType), @@ -46,18 +46,17 @@ CancellationToken cancellationToken } private static bool HasScriptModuleAttribute(INamedTypeSymbol type) - { - return type.GetAttributes() - .Any(attribute => - { - var attributeClass = attribute.AttributeClass; + => type.GetAttributes() + .Any( + attribute => + { + var attributeClass = attribute.AttributeClass; - return attributeClass is not null - && attributeClass.MetadataName == ScriptModuleAttributeMetadataName - && attributeClass.ContainingNamespace.ToDisplayString() == ScriptModuleAttributeNamespace; - } - ); - } + return attributeClass is not null && + attributeClass.MetadataName == ScriptModuleAttributeMetadataName && + attributeClass.ContainingNamespace.ToDisplayString() == ScriptModuleAttributeNamespace; + } + ); private static void Execute( SourceProductionContext context, @@ -88,7 +87,8 @@ ImmutableArray candidates } } - supported.Sort(static (left, right) => string.Compare( + supported.Sort( + static (left, right) => string.Compare( left.ScriptModuleTypeName, right.ScriptModuleTypeName, StringComparison.Ordinal diff --git a/src/SquidStd.Generators/Services/StdServiceRegistrationGenerator.cs b/src/SquidStd.Generators/Services/StdServiceRegistrationGenerator.cs index 2fd7aae3..890ed61c 100644 --- a/src/SquidStd.Generators/Services/StdServiceRegistrationGenerator.cs +++ b/src/SquidStd.Generators/Services/StdServiceRegistrationGenerator.cs @@ -35,13 +35,13 @@ CancellationToken cancellationToken var serviceType = GetServiceType(attribute); var priority = GetIntNamedArgument(attribute, "Priority"); - var isSupported = serviceType is not null - && GeneratorSymbolHelpers.IsConcreteNonGenericClass(implementationType) - && GeneratorSymbolHelpers.IsAccessibleFromGeneratedSource(implementationType) - && GeneratorSymbolHelpers.IsAccessibleFromGeneratedSource(serviceType) - && GeneratorSymbolHelpers.IsAssignableTo(implementationType, serviceType); + var isSupported = serviceType is not null && + GeneratorSymbolHelpers.IsConcreteNonGenericClass(implementationType) && + GeneratorSymbolHelpers.IsAccessibleFromGeneratedSource(implementationType) && + GeneratorSymbolHelpers.IsAccessibleFromGeneratedSource(serviceType) && + GeneratorSymbolHelpers.IsAssignableTo(implementationType, serviceType); - return new StdServiceRegistrationCandidate( + return new( serviceType is null ? string.Empty : GeneratorSymbolHelpers.FullyQualified(serviceType), GeneratorSymbolHelpers.FullyQualified(implementationType), GeneratorSymbolHelpers.DisplayName(implementationType), @@ -95,13 +95,15 @@ private static void Execute(SourceProductionContext context, ImmutableArray string.Compare( + supported.Sort( + static (left, right) => string.Compare( left.ImplementationTypeName, right.ImplementationTypeName, StringComparison.Ordinal diff --git a/src/SquidStd.Generators/SquidStd.Generators.csproj b/src/SquidStd.Generators/SquidStd.Generators.csproj index e931f201..f9d941dc 100644 --- a/src/SquidStd.Generators/SquidStd.Generators.csproj +++ b/src/SquidStd.Generators/SquidStd.Generators.csproj @@ -14,8 +14,8 @@ - - + + diff --git a/src/SquidStd.Generators/Workers/JobHandlerRegistrationGenerator.cs b/src/SquidStd.Generators/Workers/JobHandlerRegistrationGenerator.cs index 59ccd643..d3c01708 100644 --- a/src/SquidStd.Generators/Workers/JobHandlerRegistrationGenerator.cs +++ b/src/SquidStd.Generators/Workers/JobHandlerRegistrationGenerator.cs @@ -31,15 +31,15 @@ CancellationToken cancellationToken cancellationToken.ThrowIfCancellationRequested(); var handlerType = (INamedTypeSymbol)context.TargetSymbol; - var isSupported = GeneratorSymbolHelpers.IsConcreteNonGenericClass(handlerType) - && GeneratorSymbolHelpers.IsAccessibleFromGeneratedSource(handlerType) - && GeneratorSymbolHelpers.ImplementsInterface( + var isSupported = GeneratorSymbolHelpers.IsConcreteNonGenericClass(handlerType) && + GeneratorSymbolHelpers.IsAccessibleFromGeneratedSource(handlerType) && + GeneratorSymbolHelpers.ImplementsInterface( handlerType, "IJobHandler", "SquidStd.Workers.Interfaces" ); - return new JobHandlerRegistrationCandidate( + return new( GeneratorSymbolHelpers.FullyQualified(handlerType), GeneratorSymbolHelpers.DisplayName(handlerType), GeneratorSymbolHelpers.PrimaryLocation(handlerType), @@ -76,7 +76,8 @@ ImmutableArray candidates } } - supported.Sort(static (left, right) => string.Compare( + supported.Sort( + static (left, right) => string.Compare( left.HandlerTypeName, right.HandlerTypeName, StringComparison.Ordinal diff --git a/src/SquidStd.Mail.Abstractions/Exceptions/MailSendException.cs b/src/SquidStd.Mail.Abstractions/Exceptions/MailSendException.cs index b7b4c6e7..d640d699 100644 --- a/src/SquidStd.Mail.Abstractions/Exceptions/MailSendException.cs +++ b/src/SquidStd.Mail.Abstractions/Exceptions/MailSendException.cs @@ -5,7 +5,5 @@ public sealed class MailSendException : Exception { /// Initializes the exception with a message and the underlying cause. public MailSendException(string message, Exception innerException) - : base(message, innerException) - { - } + : base(message, innerException) { } } diff --git a/src/SquidStd.Mail.Abstractions/Interfaces/IMailReader.cs b/src/SquidStd.Mail.Abstractions/Interfaces/IMailReader.cs index 4ba210f8..3038bfb5 100644 --- a/src/SquidStd.Mail.Abstractions/Interfaces/IMailReader.cs +++ b/src/SquidStd.Mail.Abstractions/Interfaces/IMailReader.cs @@ -6,8 +6,8 @@ namespace SquidStd.Mail.Abstractions.Interfaces; public interface IMailReader { /// - /// Connects, fetches the new (unseen) messages, marks them seen / deletes them per options, disconnects, - /// and returns the parsed messages. + /// Connects, fetches the new (unseen) messages, marks them seen / deletes them per options, disconnects, + /// and returns the parsed messages. /// Task> FetchNewAsync(CancellationToken cancellationToken = default); } diff --git a/src/SquidStd.Mail.MailKit/Extensions/MailRegistrationExtensions.cs b/src/SquidStd.Mail.MailKit/Extensions/MailRegistrationExtensions.cs index 7faf1e79..9189363f 100644 --- a/src/SquidStd.Mail.MailKit/Extensions/MailRegistrationExtensions.cs +++ b/src/SquidStd.Mail.MailKit/Extensions/MailRegistrationExtensions.cs @@ -16,8 +16,8 @@ public static class MailRegistrationExtensions extension(IContainer container) { /// - /// Registers a single mailbox poller: the options, the protocol-specific , the - /// polling service, and the timer-wheel pump (only if not already registered). + /// Registers a single mailbox poller: the options, the protocol-specific , the + /// polling service, and the timer-wheel pump (only if not already registered). /// public IContainer AddMail(MailOptions options) { diff --git a/src/SquidStd.Mail.MailKit/README.md b/src/SquidStd.Mail.MailKit/README.md index 0465aa6c..0117e9bf 100644 --- a/src/SquidStd.Mail.MailKit/README.md +++ b/src/SquidStd.Mail.MailKit/README.md @@ -65,12 +65,12 @@ await sender.SendAsync(new OutgoingMailMessage ## Key types -| Type | Purpose | -|------|---------| -| `MailRegistrationExtensions` | `AddMail(...)` registration (IMAP/POP3 polling). | -| `MailSenderRegistrationExtensions` | `AddMailSender(...)` registration (SMTP). | -| `ImapMailReader` / `Pop3MailReader` | `IMailReader` implementations. | -| `MailKitMailSender` | `IMailSender` implementation over SMTP. | +| Type | Purpose | +|-------------------------------------|--------------------------------------------------| +| `MailRegistrationExtensions` | `AddMail(...)` registration (IMAP/POP3 polling). | +| `MailSenderRegistrationExtensions` | `AddMailSender(...)` registration (SMTP). | +| `ImapMailReader` / `Pop3MailReader` | `IMailReader` implementations. | +| `MailKitMailSender` | `IMailSender` implementation over SMTP. | ## Related diff --git a/src/SquidStd.Mail.MailKit/Services/MailPollingService.cs b/src/SquidStd.Mail.MailKit/Services/MailPollingService.cs index 7ae40175..0307a2aa 100644 --- a/src/SquidStd.Mail.MailKit/Services/MailPollingService.cs +++ b/src/SquidStd.Mail.MailKit/Services/MailPollingService.cs @@ -89,13 +89,9 @@ public async Task PollOnceAsync() } private void OnTick() - { - _ = PollOnceAsync(); - } + => _ = PollOnceAsync(); /// public void Dispose() - { - _gate.Dispose(); - } + => _gate.Dispose(); } diff --git a/src/SquidStd.Mail.MailKit/Services/MimeMessageMapper.cs b/src/SquidStd.Mail.MailKit/Services/MimeMessageMapper.cs index 8d959901..52335d34 100644 --- a/src/SquidStd.Mail.MailKit/Services/MimeMessageMapper.cs +++ b/src/SquidStd.Mail.MailKit/Services/MimeMessageMapper.cs @@ -15,9 +15,9 @@ public static MailMessage Map(MimeMessage message, bool includeAttachmentContent var to = message.To.Mailboxes.Select(ToAddress).ToArray(); var cc = message.Cc.Mailboxes.Select(ToAddress).ToArray(); var attachments = message.Attachments - .OfType() - .Select(part => ToAttachment(part, includeAttachmentContent)) - .ToArray(); + .OfType() + .Select(part => ToAttachment(part, includeAttachmentContent)) + .ToArray(); byte[]? rawEml = null; @@ -28,7 +28,7 @@ public static MailMessage Map(MimeMessage message, bool includeAttachmentContent rawEml = stream.ToArray(); } - return new MailMessage( + return new( from, to, cc, @@ -43,9 +43,7 @@ public static MailMessage Map(MimeMessage message, bool includeAttachmentContent } private static MailAddress ToAddress(MailboxAddress mailbox) - { - return new MailAddress(mailbox.Name ?? string.Empty, mailbox.Address); - } + => new(mailbox.Name ?? string.Empty, mailbox.Address); private static MailAttachment ToAttachment(MimePart part, bool includeContent) { @@ -65,6 +63,6 @@ private static MailAttachment ToAttachment(MimePart part, bool includeContent) var fileName = part.FileName ?? string.Empty; - return new MailAttachment(fileName, part.ContentType.MimeType, size, content); + return new(fileName, part.ContentType.MimeType, size, content); } } diff --git a/src/SquidStd.Mail.MailKit/Services/OutgoingMessageMapper.cs b/src/SquidStd.Mail.MailKit/Services/OutgoingMessageMapper.cs index c589197b..7ac2bd8b 100644 --- a/src/SquidStd.Mail.MailKit/Services/OutgoingMessageMapper.cs +++ b/src/SquidStd.Mail.MailKit/Services/OutgoingMessageMapper.cs @@ -44,7 +44,5 @@ public static MimeMessage ToMimeMessage(OutgoingMailMessage message, SmtpOptions } private static MailboxAddress ToMailbox(MailAddress address) - { - return new MailboxAddress(address.Name, address.Address); - } + => new(address.Name, address.Address); } diff --git a/src/SquidStd.Mail.Queue/Extensions/MailQueueRegistrationExtensions.cs b/src/SquidStd.Mail.Queue/Extensions/MailQueueRegistrationExtensions.cs index 75313393..5cdc396a 100644 --- a/src/SquidStd.Mail.Queue/Extensions/MailQueueRegistrationExtensions.cs +++ b/src/SquidStd.Mail.Queue/Extensions/MailQueueRegistrationExtensions.cs @@ -12,8 +12,8 @@ public static class MailQueueRegistrationExtensions extension(IContainer container) { /// - /// Registers the mail queue and its background consumer. Requires IMessageQueue (messaging) and - /// IMailSender (the SMTP sender) to be registered already. + /// Registers the mail queue and its background consumer. Requires IMessageQueue (messaging) and + /// IMailSender (the SMTP sender) to be registered already. /// public IContainer AddMailQueue(MailQueueOptions? options = null) { diff --git a/src/SquidStd.Mail.Queue/README.md b/src/SquidStd.Mail.Queue/README.md index 4d118cdd..889953dc 100644 --- a/src/SquidStd.Mail.Queue/README.md +++ b/src/SquidStd.Mail.Queue/README.md @@ -37,13 +37,13 @@ Retry/backoff/dead-letter are configured via `MessagingOptions` (`MaxDeliveryAtt ## Key types -| Type | Purpose | -|------|---------| -| `IMailQueue` | Enqueue an `OutgoingMailMessage` for background delivery. | -| `MailQueue` | `IMailQueue` implementation over the SquidStd messaging queue. | -| `MailSendConsumerService` | Background consumer that sends queued messages via `IMailSender`. | -| `MailQueueRegistrationExtensions` | `AddMailQueue(...)` registration. | -| `MailQueueOptions` | Queue name and send options. | +| Type | Purpose | +|-----------------------------------|-------------------------------------------------------------------| +| `IMailQueue` | Enqueue an `OutgoingMailMessage` for background delivery. | +| `MailQueue` | `IMailQueue` implementation over the SquidStd messaging queue. | +| `MailSendConsumerService` | Background consumer that sends queued messages via `IMailSender`. | +| `MailQueueRegistrationExtensions` | `AddMailQueue(...)` registration. | +| `MailQueueOptions` | Queue name and send options. | ## Related diff --git a/src/SquidStd.Mail.Queue/Services/MailSendConsumerService.cs b/src/SquidStd.Mail.Queue/Services/MailSendConsumerService.cs index a5bdf7bc..8a44a66d 100644 --- a/src/SquidStd.Mail.Queue/Services/MailSendConsumerService.cs +++ b/src/SquidStd.Mail.Queue/Services/MailSendConsumerService.cs @@ -8,8 +8,8 @@ namespace SquidStd.Mail.Queue.Services; /// -/// Consumes queued outbound messages and sends them via . Exceptions propagate so the -/// messaging layer retries / dead-letters. +/// Consumes queued outbound messages and sends them via . Exceptions propagate so the +/// messaging layer retries / dead-letters. /// public sealed class MailSendConsumerService : ISquidStdService, IQueueMessageListenerAsync { @@ -28,9 +28,7 @@ public MailSendConsumerService(IMessageQueue queue, IMailSender sender, MailQueu /// public Task HandleAsync(OutgoingMailMessage message, CancellationToken cancellationToken) - { - return _sender.SendAsync(message, cancellationToken); - } + => _sender.SendAsync(message, cancellationToken); /// public ValueTask StartAsync(CancellationToken cancellationToken = default) diff --git a/src/SquidStd.Messaging.Abstractions/Data/Config/MessagingConnectionString.cs b/src/SquidStd.Messaging.Abstractions/Data/Config/MessagingConnectionString.cs index c531dddc..9ca7ccd6 100644 --- a/src/SquidStd.Messaging.Abstractions/Data/Config/MessagingConnectionString.cs +++ b/src/SquidStd.Messaging.Abstractions/Data/Config/MessagingConnectionString.cs @@ -4,7 +4,7 @@ namespace SquidStd.Messaging.Abstractions.Data.Config; /// -/// Parsed messaging connection string of the form scheme://[user:pass@]host[:port][/vhost][?params]. +/// Parsed messaging connection string of the form scheme://[user:pass@]host[:port][/vhost][?params]. /// public sealed class MessagingConnectionString { @@ -68,14 +68,14 @@ public static MessagingConnectionString Parse(string connectionString) var virtualHost = uri.AbsolutePath.Trim('/'); var query = HttpUtility.ParseQueryString(uri.Query); var parameters = query.AllKeys - .Where(static key => key is not null) - .ToFrozenDictionary( - key => key!, - key => query[key] ?? string.Empty, - StringComparer.OrdinalIgnoreCase - ); - - return new MessagingConnectionString( + .Where(static key => key is not null) + .ToFrozenDictionary( + key => key!, + key => query[key] ?? string.Empty, + StringComparer.OrdinalIgnoreCase + ); + + return new( uri.Scheme, uri.Host, uri.Port > 0 ? uri.Port : null, @@ -88,17 +88,15 @@ public static MessagingConnectionString Parse(string connectionString) /// Builds from the query parameters. public MessagingOptions ToMessagingOptions() - { - return new MessagingOptions + => new() { MaxDeliveryAttempts = Parameters.TryGetValue("maxDeliveryAttempts", out var max) && int.TryParse(max, out var parsedMax) - ? parsedMax - : 3, + ? parsedMax + : 3, DeadLetterQueueSuffix = Parameters.TryGetValue("deadLetterSuffix", out var suffix) ? suffix : ".dlq", RetryDelay = Parameters.TryGetValue("retryDelayMs", out var delay) && int.TryParse(delay, out var parsedDelay) - ? TimeSpan.FromMilliseconds(parsedDelay) - : TimeSpan.Zero + ? TimeSpan.FromMilliseconds(parsedDelay) + : TimeSpan.Zero }; - } } diff --git a/src/SquidStd.Messaging.Abstractions/Data/Config/MessagingOptions.cs b/src/SquidStd.Messaging.Abstractions/Data/Config/MessagingOptions.cs index 482f174b..97afa226 100644 --- a/src/SquidStd.Messaging.Abstractions/Data/Config/MessagingOptions.cs +++ b/src/SquidStd.Messaging.Abstractions/Data/Config/MessagingOptions.cs @@ -1,7 +1,7 @@ namespace SquidStd.Messaging.Abstractions.Data.Config; /// -/// Configuration for the messaging system. +/// Configuration for the messaging system. /// public sealed class MessagingOptions { diff --git a/src/SquidStd.Messaging.Abstractions/Data/Events/TopicMessageEvent.cs b/src/SquidStd.Messaging.Abstractions/Data/Events/TopicMessageEvent.cs index b64fc0cd..b3a43554 100644 --- a/src/SquidStd.Messaging.Abstractions/Data/Events/TopicMessageEvent.cs +++ b/src/SquidStd.Messaging.Abstractions/Data/Events/TopicMessageEvent.cs @@ -3,7 +3,7 @@ namespace SquidStd.Messaging.Abstractions.Data.Events; /// -/// Event published on the in-process event bus when a topic message is bridged. is the -/// deserialized message. +/// Event published on the in-process event bus when a topic message is bridged. is the +/// deserialized message. /// public sealed record TopicMessageEvent(string Topic, object Data) : IEvent; diff --git a/src/SquidStd.Messaging.Abstractions/Interfaces/IMessageQueue.cs b/src/SquidStd.Messaging.Abstractions/Interfaces/IMessageQueue.cs index 0fd58df5..32f7011e 100644 --- a/src/SquidStd.Messaging.Abstractions/Interfaces/IMessageQueue.cs +++ b/src/SquidStd.Messaging.Abstractions/Interfaces/IMessageQueue.cs @@ -1,7 +1,7 @@ namespace SquidStd.Messaging.Abstractions.Interfaces; /// -/// Typed facade for publishing to and subscribing to named queues. +/// Typed facade for publishing to and subscribing to named queues. /// public interface IMessageQueue { diff --git a/src/SquidStd.Messaging.Abstractions/Interfaces/IMessageTopic.cs b/src/SquidStd.Messaging.Abstractions/Interfaces/IMessageTopic.cs index 09872ecc..e1b1c2be 100644 --- a/src/SquidStd.Messaging.Abstractions/Interfaces/IMessageTopic.cs +++ b/src/SquidStd.Messaging.Abstractions/Interfaces/IMessageTopic.cs @@ -1,7 +1,7 @@ namespace SquidStd.Messaging.Abstractions.Interfaces; /// -/// Typed facade for publishing to and subscribing to topics (fan-out). +/// Typed facade for publishing to and subscribing to topics (fan-out). /// public interface IMessageTopic { diff --git a/src/SquidStd.Messaging.Abstractions/Interfaces/IMessagingMetrics.cs b/src/SquidStd.Messaging.Abstractions/Interfaces/IMessagingMetrics.cs index 4b12e229..4ebcdb4e 100644 --- a/src/SquidStd.Messaging.Abstractions/Interfaces/IMessagingMetrics.cs +++ b/src/SquidStd.Messaging.Abstractions/Interfaces/IMessagingMetrics.cs @@ -1,7 +1,7 @@ namespace SquidStd.Messaging.Abstractions.Interfaces; /// -/// Sink for messaging metric events. Implementations must be thread-safe. +/// Sink for messaging metric events. Implementations must be thread-safe. /// public interface IMessagingMetrics { diff --git a/src/SquidStd.Messaging.Abstractions/Interfaces/IQueueMessageListener.cs b/src/SquidStd.Messaging.Abstractions/Interfaces/IQueueMessageListener.cs index a8d9d46a..b384b5f1 100644 --- a/src/SquidStd.Messaging.Abstractions/Interfaces/IQueueMessageListener.cs +++ b/src/SquidStd.Messaging.Abstractions/Interfaces/IQueueMessageListener.cs @@ -1,7 +1,7 @@ namespace SquidStd.Messaging.Abstractions.Interfaces; /// -/// Handles a queue message synchronously. +/// Handles a queue message synchronously. /// /// The message payload type. public interface IQueueMessageListener diff --git a/src/SquidStd.Messaging.Abstractions/Interfaces/IQueueMessageListenerAsync.cs b/src/SquidStd.Messaging.Abstractions/Interfaces/IQueueMessageListenerAsync.cs index 90f18c18..e7708457 100644 --- a/src/SquidStd.Messaging.Abstractions/Interfaces/IQueueMessageListenerAsync.cs +++ b/src/SquidStd.Messaging.Abstractions/Interfaces/IQueueMessageListenerAsync.cs @@ -1,7 +1,7 @@ namespace SquidStd.Messaging.Abstractions.Interfaces; /// -/// Handles a queue message asynchronously. +/// Handles a queue message asynchronously. /// /// The message payload type. public interface IQueueMessageListenerAsync diff --git a/src/SquidStd.Messaging.Abstractions/Interfaces/IQueueProvider.cs b/src/SquidStd.Messaging.Abstractions/Interfaces/IQueueProvider.cs index d7603f7e..18e26062 100644 --- a/src/SquidStd.Messaging.Abstractions/Interfaces/IQueueProvider.cs +++ b/src/SquidStd.Messaging.Abstractions/Interfaces/IQueueProvider.cs @@ -3,7 +3,7 @@ namespace SquidStd.Messaging.Abstractions.Interfaces; /// -/// Byte-level queue transport owning buffering, round-robin delivery, retry and dead-lettering. +/// Byte-level queue transport owning buffering, round-robin delivery, retry and dead-lettering. /// public interface IQueueProvider : ISquidStdService, IAsyncDisposable { diff --git a/src/SquidStd.Messaging.Abstractions/Interfaces/ITopicEventBridge.cs b/src/SquidStd.Messaging.Abstractions/Interfaces/ITopicEventBridge.cs index 9cf69bac..2ba8483c 100644 --- a/src/SquidStd.Messaging.Abstractions/Interfaces/ITopicEventBridge.cs +++ b/src/SquidStd.Messaging.Abstractions/Interfaces/ITopicEventBridge.cs @@ -1,8 +1,8 @@ namespace SquidStd.Messaging.Abstractions.Interfaces; /// -/// Bridges a topic into the in-process event bus: each message of type T on the topic is republished -/// as a TopicMessageEvent. +/// Bridges a topic into the in-process event bus: each message of type T on the topic is republished +/// as a TopicMessageEvent. /// public interface ITopicEventBridge { diff --git a/src/SquidStd.Messaging.Abstractions/Interfaces/ITopicProvider.cs b/src/SquidStd.Messaging.Abstractions/Interfaces/ITopicProvider.cs index 3deffb58..c5882dc4 100644 --- a/src/SquidStd.Messaging.Abstractions/Interfaces/ITopicProvider.cs +++ b/src/SquidStd.Messaging.Abstractions/Interfaces/ITopicProvider.cs @@ -3,8 +3,8 @@ namespace SquidStd.Messaging.Abstractions.Interfaces; /// -/// Byte-level publish/subscribe transport: every current subscriber of a topic receives every message -/// (transient, at-most-once fan-out). +/// Byte-level publish/subscribe transport: every current subscriber of a topic receives every message +/// (transient, at-most-once fan-out). /// public interface ITopicProvider : ISquidStdService, IAsyncDisposable { diff --git a/src/SquidStd.Messaging.Abstractions/Services/MessageQueue.cs b/src/SquidStd.Messaging.Abstractions/Services/MessageQueue.cs index b1f2581b..8404b4a8 100644 --- a/src/SquidStd.Messaging.Abstractions/Services/MessageQueue.cs +++ b/src/SquidStd.Messaging.Abstractions/Services/MessageQueue.cs @@ -4,8 +4,8 @@ namespace SquidStd.Messaging.Abstractions.Services; /// -/// Typed facade over an : serializes outgoing messages and -/// deserializes incoming payloads before handing them to typed listeners. +/// Typed facade over an : serializes outgoing messages and +/// deserializes incoming payloads before handing them to typed listeners. /// public sealed class MessageQueue : IMessageQueue { @@ -22,9 +22,7 @@ public MessageQueue(IQueueProvider provider, IDataSerializer serializer, IDataDe /// public Task PublishAsync(string queueName, TMessage message, CancellationToken cancellationToken = default) - { - return _provider.PublishAsync(queueName, _serializer.Serialize(message), cancellationToken); - } + => _provider.PublishAsync(queueName, _serializer.Serialize(message), cancellationToken); /// public IDisposable Subscribe(string queueName, IQueueMessageListener listener) diff --git a/src/SquidStd.Messaging.Abstractions/Services/MessageTopic.cs b/src/SquidStd.Messaging.Abstractions/Services/MessageTopic.cs index 84085782..1b2de1dc 100644 --- a/src/SquidStd.Messaging.Abstractions/Services/MessageTopic.cs +++ b/src/SquidStd.Messaging.Abstractions/Services/MessageTopic.cs @@ -4,8 +4,8 @@ namespace SquidStd.Messaging.Abstractions.Services; /// -/// Typed facade over an : serializes outgoing messages and deserializes -/// incoming payloads. +/// Typed facade over an : serializes outgoing messages and deserializes +/// incoming payloads. /// public sealed class MessageTopic : IMessageTopic { @@ -22,9 +22,7 @@ public MessageTopic(ITopicProvider provider, IDataSerializer serializer, IDataDe /// public Task PublishAsync(string topic, TMessage message, CancellationToken cancellationToken = default) - { - return _provider.PublishAsync(topic, _serializer.Serialize(message), cancellationToken); - } + => _provider.PublishAsync(topic, _serializer.Serialize(message), cancellationToken); /// public IDisposable Subscribe(string topic, Func handler) diff --git a/src/SquidStd.Messaging.Abstractions/Services/MessagingMetricsProvider.cs b/src/SquidStd.Messaging.Abstractions/Services/MessagingMetricsProvider.cs index ef147307..626844b6 100644 --- a/src/SquidStd.Messaging.Abstractions/Services/MessagingMetricsProvider.cs +++ b/src/SquidStd.Messaging.Abstractions/Services/MessagingMetricsProvider.cs @@ -7,7 +7,7 @@ namespace SquidStd.Messaging.Abstractions.Services; /// -/// Accumulates messaging metrics and exposes them to the metrics collection system. +/// Accumulates messaging metrics and exposes them to the metrics collection system. /// public sealed class MessagingMetricsProvider : IMessagingMetrics, IMetricProvider { @@ -18,45 +18,31 @@ public sealed class MessagingMetricsProvider : IMessagingMetrics, IMetricProvide /// public void OnDeadLettered(string queueName) - { - Interlocked.Increment(ref Counters(queueName).DeadLettered); - } + => Interlocked.Increment(ref Counters(queueName).DeadLettered); /// public void OnDelivered(string queueName) - { - Interlocked.Increment(ref Counters(queueName).Delivered); - } + => Interlocked.Increment(ref Counters(queueName).Delivered); /// public void OnFailed(string queueName) - { - Interlocked.Increment(ref Counters(queueName).Failed); - } + => Interlocked.Increment(ref Counters(queueName).Failed); /// public void OnPublished(string queueName) - { - Interlocked.Increment(ref Counters(queueName).Published); - } + => Interlocked.Increment(ref Counters(queueName).Published); /// public void OnRetried(string queueName) - { - Interlocked.Increment(ref Counters(queueName).Retried); - } + => Interlocked.Increment(ref Counters(queueName).Retried); /// public void SetQueueDepth(string queueName, int depth) - { - Volatile.Write(ref Counters(queueName).Depth, depth); - } + => Volatile.Write(ref Counters(queueName).Depth, depth); /// public void SetSubscriberCount(string queueName, int count) - { - Volatile.Write(ref Counters(queueName).Subscribers, count); - } + => Volatile.Write(ref Counters(queueName).Subscribers, count); /// public ValueTask> CollectAsync(CancellationToken cancellationToken = default) @@ -67,37 +53,27 @@ public ValueTask> CollectAsync(CancellationToken can { var tags = new Dictionary(StringComparer.Ordinal) { ["queue"] = queueName }; + samples.Add(new("published", Interlocked.Read(ref counters.Published), Tags: tags, Type: MetricType.Counter)); + samples.Add(new("delivered", Interlocked.Read(ref counters.Delivered), Tags: tags, Type: MetricType.Counter)); + samples.Add(new("failed", Interlocked.Read(ref counters.Failed), Tags: tags, Type: MetricType.Counter)); + samples.Add(new("retried", Interlocked.Read(ref counters.Retried), Tags: tags, Type: MetricType.Counter)); samples.Add( - new MetricSample("published", Interlocked.Read(ref counters.Published), Tags: tags, Type: MetricType.Counter) - ); - samples.Add( - new MetricSample("delivered", Interlocked.Read(ref counters.Delivered), Tags: tags, Type: MetricType.Counter) - ); - samples.Add( - new MetricSample("failed", Interlocked.Read(ref counters.Failed), Tags: tags, Type: MetricType.Counter) - ); - samples.Add( - new MetricSample("retried", Interlocked.Read(ref counters.Retried), Tags: tags, Type: MetricType.Counter) - ); - samples.Add( - new MetricSample( + new( "dead_lettered", Interlocked.Read(ref counters.DeadLettered), Tags: tags, Type: MetricType.Counter ) ); - samples.Add(new MetricSample("queue_depth", Volatile.Read(ref counters.Depth), Tags: tags)); - samples.Add(new MetricSample("subscribers", Volatile.Read(ref counters.Subscribers), Tags: tags)); + samples.Add(new("queue_depth", Volatile.Read(ref counters.Depth), Tags: tags)); + samples.Add(new("subscribers", Volatile.Read(ref counters.Subscribers), Tags: tags)); } return ValueTask.FromResult>(samples); } private QueueCounters Counters(string queueName) - { - return _queues.GetOrAdd(queueName, static _ => new QueueCounters()); - } + => _queues.GetOrAdd(queueName, static _ => new()); private sealed class QueueCounters { diff --git a/src/SquidStd.Messaging.Abstractions/Services/NoOpMessagingMetrics.cs b/src/SquidStd.Messaging.Abstractions/Services/NoOpMessagingMetrics.cs index a51d632d..49fbfb26 100644 --- a/src/SquidStd.Messaging.Abstractions/Services/NoOpMessagingMetrics.cs +++ b/src/SquidStd.Messaging.Abstractions/Services/NoOpMessagingMetrics.cs @@ -3,38 +3,24 @@ namespace SquidStd.Messaging.Abstractions.Services; /// -/// Metrics sink that ignores all events. Used when no metrics are configured. +/// Metrics sink that ignores all events. Used when no metrics are configured. /// public sealed class NoOpMessagingMetrics : IMessagingMetrics { /// Shared instance. public static NoOpMessagingMetrics Instance { get; } = new(); - public void OnDeadLettered(string queueName) - { - } + public void OnDeadLettered(string queueName) { } - public void OnDelivered(string queueName) - { - } + public void OnDelivered(string queueName) { } - public void OnFailed(string queueName) - { - } + public void OnFailed(string queueName) { } - public void OnPublished(string queueName) - { - } + public void OnPublished(string queueName) { } - public void OnRetried(string queueName) - { - } + public void OnRetried(string queueName) { } - public void SetQueueDepth(string queueName, int depth) - { - } + public void SetQueueDepth(string queueName, int depth) { } - public void SetSubscriberCount(string queueName, int count) - { - } + public void SetSubscriberCount(string queueName, int count) { } } diff --git a/src/SquidStd.Messaging.Abstractions/Services/TopicEventBridge.cs b/src/SquidStd.Messaging.Abstractions/Services/TopicEventBridge.cs index 03e84c44..5e45bda4 100644 --- a/src/SquidStd.Messaging.Abstractions/Services/TopicEventBridge.cs +++ b/src/SquidStd.Messaging.Abstractions/Services/TopicEventBridge.cs @@ -5,8 +5,8 @@ namespace SquidStd.Messaging.Abstractions.Services; /// -/// One-way Topic → EventBus bridge. Subscribes a topic and republishes each message as a -/// on the . +/// One-way Topic → EventBus bridge. Subscribes a topic and republishes each message as a +/// on the . /// public sealed class TopicEventBridge : ITopicEventBridge { @@ -21,10 +21,8 @@ public TopicEventBridge(IMessageTopic topic, IEventBus eventBus) /// public IDisposable Bridge(string topic) - { - return _topic.Subscribe( + => _topic.Subscribe( topic, (data, cancellationToken) => _eventBus.PublishAsync(new TopicMessageEvent(topic, data!), cancellationToken) ); - } } diff --git a/src/SquidStd.Messaging.RabbitMq/Data/Config/RabbitMqOptions.cs b/src/SquidStd.Messaging.RabbitMq/Data/Config/RabbitMqOptions.cs index f4f90d1b..4e939ab7 100644 --- a/src/SquidStd.Messaging.RabbitMq/Data/Config/RabbitMqOptions.cs +++ b/src/SquidStd.Messaging.RabbitMq/Data/Config/RabbitMqOptions.cs @@ -1,7 +1,7 @@ namespace SquidStd.Messaging.RabbitMq.Data.Config; /// -/// Connection options for the RabbitMQ queue provider. +/// Connection options for the RabbitMQ queue provider. /// public sealed class RabbitMqOptions { diff --git a/src/SquidStd.Messaging.RabbitMq/Extensions/RabbitMqMessagingRegistrationExtensions.cs b/src/SquidStd.Messaging.RabbitMq/Extensions/RabbitMqMessagingRegistrationExtensions.cs index 5f3ad837..da9fe84f 100644 --- a/src/SquidStd.Messaging.RabbitMq/Extensions/RabbitMqMessagingRegistrationExtensions.cs +++ b/src/SquidStd.Messaging.RabbitMq/Extensions/RabbitMqMessagingRegistrationExtensions.cs @@ -11,7 +11,7 @@ namespace SquidStd.Messaging.RabbitMq.Extensions; /// -/// DryIoc registration helpers for the RabbitMQ messaging provider. +/// DryIoc registration helpers for the RabbitMQ messaging provider. /// public static class RabbitMqMessagingRegistrationExtensions { @@ -80,8 +80,8 @@ public IContainer AddRabbitMqMessaging(string connectionString) Password = cs.Password ?? "guest", PrefetchCount = cs.Parameters.TryGetValue("prefetch", out var prefetch) && ushort.TryParse(prefetch, out var parsed) - ? parsed - : (ushort)10 + ? parsed + : (ushort)10 }; return container.AddRabbitMqMessaging(options, cs.ToMessagingOptions()); diff --git a/src/SquidStd.Messaging.RabbitMq/Services/RabbitMqQueueProvider.cs b/src/SquidStd.Messaging.RabbitMq/Services/RabbitMqQueueProvider.cs index 1478923e..71cec08b 100644 --- a/src/SquidStd.Messaging.RabbitMq/Services/RabbitMqQueueProvider.cs +++ b/src/SquidStd.Messaging.RabbitMq/Services/RabbitMqQueueProvider.cs @@ -10,8 +10,8 @@ namespace SquidStd.Messaging.RabbitMq.Services; /// -/// RabbitMQ : named queues map to quorum queues with a delivery limit -/// and a dead-letter exchange; round-robin is the broker's native competing-consumers behaviour. +/// RabbitMQ : named queues map to quorum queues with a delivery limit +/// and a dead-letter exchange; round-robin is the broker's native competing-consumers behaviour. /// public sealed class RabbitMqQueueProvider : IQueueProvider { @@ -120,9 +120,7 @@ public async ValueTask StartAsync(CancellationToken cancellationToken = default) /// public ValueTask StopAsync(CancellationToken cancellationToken = default) - { - return DisposeAsync(); - } + => DisposeAsync(); /// public IDisposable Subscribe(string queueName, Func, CancellationToken, Task> handler) @@ -228,9 +226,7 @@ Func, CancellationToken, Task> handler } public void Start() - { - StartAsync().GetAwaiter().GetResult(); - } + => StartAsync().GetAwaiter().GetResult(); private async Task OnReceivedAsync(object sender, BasicDeliverEventArgs args) { diff --git a/src/SquidStd.Messaging.RabbitMq/Services/RabbitMqTopicProvider.cs b/src/SquidStd.Messaging.RabbitMq/Services/RabbitMqTopicProvider.cs index 58bb3361..2916fc38 100644 --- a/src/SquidStd.Messaging.RabbitMq/Services/RabbitMqTopicProvider.cs +++ b/src/SquidStd.Messaging.RabbitMq/Services/RabbitMqTopicProvider.cs @@ -7,8 +7,8 @@ namespace SquidStd.Messaging.RabbitMq.Services; /// -/// RabbitMQ : topics map to fanout exchanges; each subscriber binds an -/// exclusive auto-delete queue and consumes with auto-ack (transient, at-most-once fan-out). +/// RabbitMQ : topics map to fanout exchanges; each subscriber binds an +/// exclusive auto-delete queue and consumes with auto-ack (transient, at-most-once fan-out). /// public sealed class RabbitMqTopicProvider : ITopicProvider { @@ -102,9 +102,7 @@ public async ValueTask StartAsync(CancellationToken cancellationToken = default) /// public ValueTask StopAsync(CancellationToken cancellationToken = default) - { - return DisposeAsync(); - } + => DisposeAsync(); /// public IDisposable Subscribe(string topic, Func, CancellationToken, Task> handler) @@ -176,9 +174,7 @@ Func, CancellationToken, Task> handler } public void Start() - { - StartAsync().GetAwaiter().GetResult(); - } + => StartAsync().GetAwaiter().GetResult(); private async Task OnReceivedAsync(object sender, BasicDeliverEventArgs args) { diff --git a/src/SquidStd.Messaging.Sqs/Data/Config/SqsOptions.cs b/src/SquidStd.Messaging.Sqs/Data/Config/SqsOptions.cs index 7298d13a..2b2a14b1 100644 --- a/src/SquidStd.Messaging.Sqs/Data/Config/SqsOptions.cs +++ b/src/SquidStd.Messaging.Sqs/Data/Config/SqsOptions.cs @@ -3,8 +3,8 @@ namespace SquidStd.Messaging.Sqs.Data.Config; /// -/// Configuration for the SQS/SNS messaging provider. Connection details live in ; -/// the remaining knobs tune SQS receive behaviour. +/// Configuration for the SQS/SNS messaging provider. Connection details live in ; +/// the remaining knobs tune SQS receive behaviour. /// public sealed class SqsOptions { diff --git a/src/SquidStd.Messaging.Sqs/Extensions/SqsMessagingRegistrationExtensions.cs b/src/SquidStd.Messaging.Sqs/Extensions/SqsMessagingRegistrationExtensions.cs index 90a9419d..6b4959ed 100644 --- a/src/SquidStd.Messaging.Sqs/Extensions/SqsMessagingRegistrationExtensions.cs +++ b/src/SquidStd.Messaging.Sqs/Extensions/SqsMessagingRegistrationExtensions.cs @@ -1,6 +1,5 @@ using System.Globalization; using DryIoc; -using SquidStd.Aws.Abstractions.Data.Config; using SquidStd.Core.Interfaces.Metrics; using SquidStd.Core.Interfaces.Serialization; using SquidStd.Core.Json; @@ -13,7 +12,7 @@ namespace SquidStd.Messaging.Sqs.Extensions; /// -/// DryIoc registration helpers for the AWS SQS/SNS messaging provider. +/// DryIoc registration helpers for the AWS SQS/SNS messaging provider. /// public static class SqsMessagingRegistrationExtensions { @@ -30,9 +29,9 @@ public static SqsOptions ParseOptions(string connectionString) ); } - return new SqsOptions + return new() { - Aws = new AwsConfigEntry + Aws = new() { Region = string.IsNullOrEmpty(cs.Host) ? "us-east-1" : cs.Host, AccessKey = cs.UserName, @@ -42,16 +41,16 @@ public static SqsOptions ParseOptions(string connectionString) }, MaxNumberOfMessages = cs.Parameters.TryGetValue("maxMessages", out var max) && int.TryParse(max, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedMax) - ? parsedMax - : 10, + ? parsedMax + : 10, VisibilityTimeout = cs.Parameters.TryGetValue("visibilityTimeoutSec", out var vis) && int.TryParse(vis, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedVis) - ? TimeSpan.FromSeconds(parsedVis) - : TimeSpan.FromSeconds(30), + ? TimeSpan.FromSeconds(parsedVis) + : TimeSpan.FromSeconds(30), WaitTimeSeconds = cs.Parameters.TryGetValue("waitTimeSec", out var wait) && int.TryParse(wait, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedWait) - ? parsedWait - : 20 + ? parsedWait + : 20 }; } diff --git a/src/SquidStd.Messaging.Sqs/Internal/AwsClientFactory.cs b/src/SquidStd.Messaging.Sqs/Internal/AwsClientFactory.cs index f7466d6b..a055526c 100644 --- a/src/SquidStd.Messaging.Sqs/Internal/AwsClientFactory.cs +++ b/src/SquidStd.Messaging.Sqs/Internal/AwsClientFactory.cs @@ -7,7 +7,7 @@ namespace SquidStd.Messaging.Sqs.Internal; /// -/// Builds AWS SDK clients/credentials from a shared . +/// Builds AWS SDK clients/credentials from a shared . /// internal static class AwsClientFactory { @@ -16,8 +16,8 @@ public static AWSCredentials Credentials(AwsConfigEntry aws) if (!string.IsNullOrWhiteSpace(aws.AccessKey) && !string.IsNullOrWhiteSpace(aws.SecretKey)) { return string.IsNullOrWhiteSpace(aws.SessionToken) - ? new BasicAWSCredentials(aws.AccessKey, aws.SecretKey) - : new SessionAWSCredentials(aws.AccessKey, aws.SecretKey, aws.SessionToken); + ? new BasicAWSCredentials(aws.AccessKey, aws.SecretKey) + : new SessionAWSCredentials(aws.AccessKey, aws.SecretKey, aws.SessionToken); } return FallbackCredentialsFactory.GetCredentials(); diff --git a/src/SquidStd.Messaging.Sqs/Internal/SqsNames.cs b/src/SquidStd.Messaging.Sqs/Internal/SqsNames.cs index 15b6a275..8436dd48 100644 --- a/src/SquidStd.Messaging.Sqs/Internal/SqsNames.cs +++ b/src/SquidStd.Messaging.Sqs/Internal/SqsNames.cs @@ -1,7 +1,7 @@ namespace SquidStd.Messaging.Sqs.Internal; /// -/// Sanitizes queue/topic names to the SQS/SNS allowed alphabet (letters, digits, '-', '_'). +/// Sanitizes queue/topic names to the SQS/SNS allowed alphabet (letters, digits, '-', '_'). /// internal static class SqsNames { @@ -17,6 +17,6 @@ public static string Sanitize(string name) buffer[i] = char.IsAsciiLetterOrDigit(c) || c is '-' or '_' ? c : '-'; } - return new string(buffer); + return new(buffer); } } diff --git a/src/SquidStd.Messaging.Sqs/README.md b/src/SquidStd.Messaging.Sqs/README.md index 6aad0023..36db5ae0 100644 --- a/src/SquidStd.Messaging.Sqs/README.md +++ b/src/SquidStd.Messaging.Sqs/README.md @@ -32,12 +32,12 @@ container.AddSqsMessaging(new SqsOptions { Aws = new AwsConfigEntry { Region = " ## Key types -| Type | Purpose | -|-------------------------------------|------------------------------------------| -| `SqsMessagingRegistrationExtensions` | `AddSqsMessaging(...)` registration. | -| `SqsQueueProvider` | SQS-backed `IQueueProvider`. | -| `SqsTopicProvider` | SNS+SQS-backed `ITopicProvider`. | -| `SqsOptions` | AWS connection + queue/topic configuration. | +| Type | Purpose | +|--------------------------------------|---------------------------------------------| +| `SqsMessagingRegistrationExtensions` | `AddSqsMessaging(...)` registration. | +| `SqsQueueProvider` | SQS-backed `IQueueProvider`. | +| `SqsTopicProvider` | SNS+SQS-backed `ITopicProvider`. | +| `SqsOptions` | AWS connection + queue/topic configuration. | ## Related diff --git a/src/SquidStd.Messaging.Sqs/Services/SqsQueueProvider.cs b/src/SquidStd.Messaging.Sqs/Services/SqsQueueProvider.cs index 7910a2a9..a0df2c5f 100644 --- a/src/SquidStd.Messaging.Sqs/Services/SqsQueueProvider.cs +++ b/src/SquidStd.Messaging.Sqs/Services/SqsQueueProvider.cs @@ -13,10 +13,10 @@ namespace SquidStd.Messaging.Sqs.Services; /// -/// AWS SQS : named queues are created with a redrive policy to a -/// "<queue><suffix>" dead-letter queue (maxReceiveCount = MaxDeliveryAttempts). Subscribers -/// long-poll; a handler that throws leaves the message un-acked so SQS redelivers and eventually -/// dead-letters it. Payloads travel base64-encoded in the message body. +/// AWS SQS : named queues are created with a redrive policy to a +/// "<queue><suffix>" dead-letter queue (maxReceiveCount = MaxDeliveryAttempts). Subscribers +/// long-poll; a handler that throws leaves the message un-acked so SQS redelivers and eventually +/// dead-letters it. Payloads travel base64-encoded in the message body. /// public sealed class SqsQueueProvider : IQueueProvider { @@ -66,7 +66,7 @@ public async Task PublishAsync( var url = await EnsureQueueAsync(queueName, cancellationToken); await client.SendMessageAsync( - new SendMessageRequest { QueueUrl = url, MessageBody = Convert.ToBase64String(payload.Span) }, + new() { QueueUrl = url, MessageBody = Convert.ToBase64String(payload.Span) }, cancellationToken ); @@ -83,9 +83,7 @@ public ValueTask StartAsync(CancellationToken cancellationToken = default) /// public ValueTask StopAsync(CancellationToken cancellationToken = default) - { - return DisposeAsync(); - } + => DisposeAsync(); /// public IDisposable Subscribe(string queueName, Func, CancellationToken, Task> handler) @@ -132,9 +130,9 @@ private async Task EnsureQueueAsync(string queueName, CancellationToken { var dlqName = name + _deadLetterSuffix; var dlqUrl = (await client.CreateQueueAsync( - new CreateQueueRequest { QueueName = dlqName }, - cancellationToken - )).QueueUrl; + new CreateQueueRequest { QueueName = dlqName }, + cancellationToken + )).QueueUrl; var dlqArn = await GetQueueArnAsync(client, dlqUrl, cancellationToken); var redrivePolicy = JsonSerializer.Serialize( @@ -146,13 +144,13 @@ private async Task EnsureQueueAsync(string queueName, CancellationToken ); url = (await client.CreateQueueAsync( - new CreateQueueRequest - { - QueueName = name, - Attributes = new Dictionary { ["RedrivePolicy"] = redrivePolicy } - }, - cancellationToken - )).QueueUrl; + new CreateQueueRequest + { + QueueName = name, + Attributes = new() { ["RedrivePolicy"] = redrivePolicy } + }, + cancellationToken + )).QueueUrl; } _queueUrls[name] = url; @@ -172,9 +170,9 @@ CancellationToken cancellationToken ) { var response = await client.GetQueueAttributesAsync( - new GetQueueAttributesRequest { QueueUrl = queueUrl, AttributeNames = ["QueueArn"] }, - cancellationToken - ); + new() { QueueUrl = queueUrl, AttributeNames = ["QueueArn"] }, + cancellationToken + ); return response.Attributes["QueueArn"]; } @@ -203,9 +201,7 @@ Func, CancellationToken, Task> handler } public void Start() - { - _loop = Task.Run(() => RunAsync(_cts.Token)); - } + => _loop = Task.Run(() => RunAsync(_cts.Token)); private async Task RunAsync(CancellationToken cancellationToken) { @@ -233,15 +229,15 @@ private async Task RunAsync(CancellationToken cancellationToken) try { response = await _client.ReceiveMessageAsync( - new ReceiveMessageRequest - { - QueueUrl = url, - MaxNumberOfMessages = _provider._options.MaxNumberOfMessages, - WaitTimeSeconds = _provider._options.WaitTimeSeconds, - VisibilityTimeout = (int)_provider._options.VisibilityTimeout.TotalSeconds - }, - cancellationToken - ); + new ReceiveMessageRequest + { + QueueUrl = url, + MaxNumberOfMessages = _provider._options.MaxNumberOfMessages, + WaitTimeSeconds = _provider._options.WaitTimeSeconds, + VisibilityTimeout = (int)_provider._options.VisibilityTimeout.TotalSeconds + }, + cancellationToken + ); } catch (OperationCanceledException) { diff --git a/src/SquidStd.Messaging.Sqs/Services/SqsTopicProvider.cs b/src/SquidStd.Messaging.Sqs/Services/SqsTopicProvider.cs index 08d1c330..fbad2a80 100644 --- a/src/SquidStd.Messaging.Sqs/Services/SqsTopicProvider.cs +++ b/src/SquidStd.Messaging.Sqs/Services/SqsTopicProvider.cs @@ -12,9 +12,9 @@ namespace SquidStd.Messaging.Sqs.Services; /// -/// SNS+SQS : a topic is an SNS topic; each subscriber gets a dedicated -/// ephemeral SQS queue subscribed to the topic with raw message delivery, long-polled and torn down -/// on dispose (transient, at-most-once fan-out). Payloads travel base64-encoded. +/// SNS+SQS : a topic is an SNS topic; each subscriber gets a dedicated +/// ephemeral SQS queue subscribed to the topic with raw message delivery, long-polled and torn down +/// on dispose (transient, at-most-once fan-out). Payloads travel base64-encoded. /// public sealed class SqsTopicProvider : ITopicProvider { @@ -56,7 +56,7 @@ public async Task PublishAsync(string topic, ReadOnlyMemory payload, Cance var arn = await EnsureTopicAsync(topic, cancellationToken); await sns.PublishAsync( - new PublishRequest { TopicArn = arn, Message = Convert.ToBase64String(payload.Span) }, + new() { TopicArn = arn, Message = Convert.ToBase64String(payload.Span) }, cancellationToken ); } @@ -73,9 +73,7 @@ public ValueTask StartAsync(CancellationToken cancellationToken = default) /// public ValueTask StopAsync(CancellationToken cancellationToken = default) - { - return DisposeAsync(); - } + => DisposeAsync(); /// public IDisposable Subscribe(string topic, Func, CancellationToken, Task> handler) @@ -152,13 +150,10 @@ Func, CancellationToken, Task> handler } public void Start() - { - _loop = Task.Run(() => RunAsync(_cts.Token)); - } + => _loop = Task.Run(() => RunAsync(_cts.Token)); private static string BuildPolicy(string queueArn, string topicArn) - { - return JsonSerializer.Serialize( + => JsonSerializer.Serialize( new { Version = "2012-10-17", @@ -175,7 +170,6 @@ private static string BuildPolicy(string queueArn, string topicArn) } } ); - } private async Task RunAsync(CancellationToken cancellationToken) { @@ -186,37 +180,37 @@ private async Task RunAsync(CancellationToken cancellationToken) var topicArn = await _provider.EnsureTopicAsync(_topic, cancellationToken); var queueName = SqsNames.Sanitize(_topic) + "-sub-" + _index; queueUrl = (await _provider._sqs!.CreateQueueAsync( - new CreateQueueRequest { QueueName = queueName }, - cancellationToken - )).QueueUrl; + new CreateQueueRequest { QueueName = queueName }, + cancellationToken + )).QueueUrl; _queueUrl = queueUrl; var attributes = await _provider._sqs.GetQueueAttributesAsync( - new GetQueueAttributesRequest { QueueUrl = queueUrl, AttributeNames = ["QueueArn"] }, - cancellationToken - ); + new() { QueueUrl = queueUrl, AttributeNames = ["QueueArn"] }, + cancellationToken + ); var queueArn = attributes.Attributes["QueueArn"]; await _provider._sqs.SetQueueAttributesAsync( - new SetQueueAttributesRequest + new() { QueueUrl = queueUrl, - Attributes = new Dictionary { ["Policy"] = BuildPolicy(queueArn, topicArn) } + Attributes = new() { ["Policy"] = BuildPolicy(queueArn, topicArn) } }, cancellationToken ); _subscriptionArn = (await _provider._sns!.SubscribeAsync( - new SubscribeRequest - { - TopicArn = topicArn, - Protocol = "sqs", - Endpoint = queueArn, - ReturnSubscriptionArn = true, - Attributes = new Dictionary { ["RawMessageDelivery"] = "true" } - }, - cancellationToken - )).SubscriptionArn; + new() + { + TopicArn = topicArn, + Protocol = "sqs", + Endpoint = queueArn, + ReturnSubscriptionArn = true, + Attributes = new() { ["RawMessageDelivery"] = "true" } + }, + cancellationToken + )).SubscriptionArn; } catch (OperationCanceledException) { @@ -236,14 +230,14 @@ await _provider._sqs.SetQueueAttributesAsync( try { response = await _provider._sqs!.ReceiveMessageAsync( - new ReceiveMessageRequest - { - QueueUrl = queueUrl, - MaxNumberOfMessages = _provider._options.MaxNumberOfMessages, - WaitTimeSeconds = _provider._options.WaitTimeSeconds - }, - cancellationToken - ); + new ReceiveMessageRequest + { + QueueUrl = queueUrl, + MaxNumberOfMessages = _provider._options.MaxNumberOfMessages, + WaitTimeSeconds = _provider._options.WaitTimeSeconds + }, + cancellationToken + ); } catch (OperationCanceledException) { diff --git a/src/SquidStd.Messaging/Extensions/MessagingRegistrationExtensions.cs b/src/SquidStd.Messaging/Extensions/MessagingRegistrationExtensions.cs index 77c97ee1..bcb407c7 100644 --- a/src/SquidStd.Messaging/Extensions/MessagingRegistrationExtensions.cs +++ b/src/SquidStd.Messaging/Extensions/MessagingRegistrationExtensions.cs @@ -10,7 +10,7 @@ namespace SquidStd.Messaging.Extensions; /// -/// DryIoc registration helpers for the in-memory messaging system. +/// DryIoc registration helpers for the in-memory messaging system. /// public static class MessagingRegistrationExtensions { @@ -18,7 +18,7 @@ public static class MessagingRegistrationExtensions extension(IContainer container) { /// - /// Registers the in-memory messaging services (facade, provider, serializer, metrics). + /// Registers the in-memory messaging services (facade, provider, serializer, metrics). /// /// Optional messaging options; defaults are used when null. /// The container for chaining. @@ -47,7 +47,7 @@ public IContainer AddInMemoryMessaging(MessagingOptions? options = null) } /// - /// Registers the in-memory messaging services from a connection string (scheme must be "memory"). + /// Registers the in-memory messaging services from a connection string (scheme must be "memory"). /// public IContainer AddInMemoryMessaging(string connectionString) { diff --git a/src/SquidStd.Messaging/Internal/InMemoryQueue.cs b/src/SquidStd.Messaging/Internal/InMemoryQueue.cs index c4f1f555..1bbacaed 100644 --- a/src/SquidStd.Messaging/Internal/InMemoryQueue.cs +++ b/src/SquidStd.Messaging/Internal/InMemoryQueue.cs @@ -3,7 +3,7 @@ namespace SquidStd.Messaging.Internal; /// -/// Per-queue in-memory state: the buffer channel, the registered handlers, and the round-robin index. +/// Per-queue in-memory state: the buffer channel, the registered handlers, and the round-robin index. /// internal sealed class InMemoryQueue { @@ -13,7 +13,7 @@ internal sealed class InMemoryQueue private int _roundRobinIndex; public Channel Channel { get; } = System.Threading.Channels.Channel.CreateUnbounded( - new UnboundedChannelOptions { SingleReader = true, SingleWriter = false } + new() { SingleReader = true, SingleWriter = false } ); public Task? ConsumerLoop { get; set; } @@ -39,15 +39,11 @@ public void AddHandler(Func, CancellationToken, Task> handl /// Decrements the buffered depth and returns the new value. public int DecrementDepth() - { - return Interlocked.Decrement(ref _depth); - } + => Interlocked.Decrement(ref _depth); /// Increments the buffered depth and returns the new value. public int IncrementDepth() - { - return Interlocked.Increment(ref _depth); - } + => Interlocked.Increment(ref _depth); /// Returns the next handler in round-robin order, or null when none are registered. public Func, CancellationToken, Task>? NextHandler() diff --git a/src/SquidStd.Messaging/Internal/QueuedMessage.cs b/src/SquidStd.Messaging/Internal/QueuedMessage.cs index 6e46f970..0ff2c295 100644 --- a/src/SquidStd.Messaging/Internal/QueuedMessage.cs +++ b/src/SquidStd.Messaging/Internal/QueuedMessage.cs @@ -1,7 +1,7 @@ namespace SquidStd.Messaging.Internal; /// -/// A buffered queue message with its delivery attempt count. +/// A buffered queue message with its delivery attempt count. /// /// The raw message payload. /// Number of delivery attempts already made (0 on first enqueue). diff --git a/src/SquidStd.Messaging/Services/InMemoryQueueProvider.cs b/src/SquidStd.Messaging/Services/InMemoryQueueProvider.cs index 21d4f14c..81772162 100644 --- a/src/SquidStd.Messaging/Services/InMemoryQueueProvider.cs +++ b/src/SquidStd.Messaging/Services/InMemoryQueueProvider.cs @@ -8,8 +8,8 @@ namespace SquidStd.Messaging.Services; /// -/// In-memory : one buffered channel + consumer loop per named queue, -/// round-robin delivery, retry and dead-lettering. +/// In-memory : one buffered channel + consumer loop per named queue, +/// round-robin delivery, retry and dead-lettering. /// public sealed class InMemoryQueueProvider : IQueueProvider { @@ -73,7 +73,7 @@ public Task PublishAsync(string queueName, ReadOnlyMemory payload, Cancell ArgumentException.ThrowIfNullOrWhiteSpace(queueName); cancellationToken.ThrowIfCancellationRequested(); - Enqueue(queueName, new QueuedMessage(payload, 0)); + Enqueue(queueName, new(payload, 0)); _metrics.OnPublished(queueName); return Task.CompletedTask; @@ -81,15 +81,11 @@ public Task PublishAsync(string queueName, ReadOnlyMemory payload, Cancell /// public ValueTask StartAsync(CancellationToken cancellationToken = default) - { - return ValueTask.CompletedTask; - } + => ValueTask.CompletedTask; /// public ValueTask StopAsync(CancellationToken cancellationToken = default) - { - return DisposeAsync(); - } + => DisposeAsync(); /// public IDisposable Subscribe(string queueName, Func, CancellationToken, Task> handler) @@ -145,13 +141,10 @@ private async Task ConsumeLoopAsync(string queueName, InMemoryQueue queue, Cance } private void Enqueue(string queueName, QueuedMessage message) - { - Write(GetOrCreate(queueName), queueName, message); - } + => Write(GetOrCreate(queueName), queueName, message); private InMemoryQueue GetOrCreate(string queueName) - { - return _queues.GetOrAdd( + => _queues.GetOrAdd( queueName, name => { @@ -164,7 +157,6 @@ private InMemoryQueue GetOrCreate(string queueName) return queue; } ); - } private void HandleFailure(string queueName, InMemoryQueue queue, QueuedMessage message, Exception exception) { @@ -186,7 +178,7 @@ private void HandleFailure(string queueName, InMemoryQueue queue, QueuedMessage nextAttempt ); _metrics.OnDeadLettered(queueName); - Enqueue(queueName + _options.DeadLetterQueueSuffix, new QueuedMessage(message.Payload, 0)); + Enqueue(queueName + _options.DeadLetterQueueSuffix, new(message.Payload, 0)); } private async Task RequeueAsync(InMemoryQueue queue, string queueName, QueuedMessage message) diff --git a/src/SquidStd.Messaging/Services/InMemoryTopicProvider.cs b/src/SquidStd.Messaging/Services/InMemoryTopicProvider.cs index 0550ebbf..22518a7a 100644 --- a/src/SquidStd.Messaging/Services/InMemoryTopicProvider.cs +++ b/src/SquidStd.Messaging/Services/InMemoryTopicProvider.cs @@ -5,8 +5,8 @@ namespace SquidStd.Messaging.Services; /// -/// In-memory : fan-out delivery to all current subscribers of a topic. -/// Transient and at-most-once; exceptions in one subscriber are isolated. +/// In-memory : fan-out delivery to all current subscribers of a topic. +/// Transient and at-most-once; exceptions in one subscriber are isolated. /// public sealed class InMemoryTopicProvider : ITopicProvider { @@ -56,15 +56,11 @@ public async Task PublishAsync(string topic, ReadOnlyMemory payload, Cance /// public ValueTask StartAsync(CancellationToken cancellationToken = default) - { - return ValueTask.CompletedTask; - } + => ValueTask.CompletedTask; /// public ValueTask StopAsync(CancellationToken cancellationToken = default) - { - return DisposeAsync(); - } + => DisposeAsync(); /// public IDisposable Subscribe(string topic, Func, CancellationToken, Task> handler) @@ -74,7 +70,7 @@ public IDisposable Subscribe(string topic, Func, Cancellati var handlers = _topics.GetOrAdd( topic, - static _ => new ConcurrentDictionary, CancellationToken, Task>>() + static _ => new() ); var id = Guid.NewGuid(); handlers[id] = handler; diff --git a/src/SquidStd.Network/Buffers/CircularBuffer.cs b/src/SquidStd.Network/Buffers/CircularBuffer.cs index 61adcf2e..d1d597ff 100644 --- a/src/SquidStd.Network/Buffers/CircularBuffer.cs +++ b/src/SquidStd.Network/Buffers/CircularBuffer.cs @@ -5,56 +5,56 @@ namespace SquidStd.Network.Buffers; /// /// -/// Circular buffer. -/// When writing to a full buffer: -/// PushBack -> removes this[0] / Front() -/// PushFront -> removes this[Size-1] / Back() -/// this implementation is inspired by -/// http://www.boost.org/doc/libs/1_53_0/libs/circular_buffer/doc/circular_buffer.html -/// because I liked their interface. +/// Circular buffer. +/// When writing to a full buffer: +/// PushBack -> removes this[0] / Front() +/// PushFront -> removes this[Size-1] / Back() +/// this implementation is inspired by +/// http://www.boost.org/doc/libs/1_53_0/libs/circular_buffer/doc/circular_buffer.html +/// because I liked their interface. /// public class CircularBuffer : IEnumerable { private readonly T[] _buffer; /// - /// The _end. Index after the last element in the buffer. + /// The _end. Index after the last element in the buffer. /// private int _end; /// - /// The _start. Index of the first element in buffer. + /// The _start. Index of the first element in buffer. /// private int _start; /// - /// Maximum capacity of the buffer. Elements pushed into the buffer after - /// maximum capacity is reached (IsFull = true), will remove an element. + /// Maximum capacity of the buffer. Elements pushed into the buffer after + /// maximum capacity is reached (IsFull = true), will remove an element. /// public int Capacity => _buffer.Length; /// - /// Boolean indicating if Circular is at full capacity. - /// Adding more elements when the buffer is full will - /// cause elements to be removed from the other end - /// of the buffer. + /// Boolean indicating if Circular is at full capacity. + /// Adding more elements when the buffer is full will + /// cause elements to be removed from the other end + /// of the buffer. /// public bool IsFull => Size == Capacity; /// - /// True if has no elements. + /// True if has no elements. /// public bool IsEmpty => Size == 0; /// - /// Current buffer size (the number of elements that the buffer has). + /// Current buffer size (the number of elements that the buffer has). /// public int Size { get; private set; } /// - /// Index access to elements in buffer. - /// Index does not loop around like when adding elements, - /// valid interval is [0;Size[ + /// Index access to elements in buffer. + /// Index does not loop around like when adding elements, + /// valid interval is [0;Size[ /// /// Index of element to access. /// Thrown when index is outside of [; Size[ interval. @@ -94,26 +94,24 @@ public T this[int index] } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// - /// Buffer capacity. Must be positive. + /// Buffer capacity. Must be positive. /// public CircularBuffer(int capacity) - : this(capacity, []) - { - } + : this(capacity, []) { } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// - /// Buffer capacity. Must be positive. + /// Buffer capacity. Must be positive. /// /// - /// Items to fill buffer with. Items length must be less than capacity. - /// Suggestion: use Skip(x).Take(y).ToArray() to build this argument from - /// any enumerable. + /// Items to fill buffer with. Items length must be less than capacity. + /// Suggestion: use Skip(x).Take(y).ToArray() to build this argument from + /// any enumerable. /// public CircularBuffer(int capacity, T[] items) { @@ -144,10 +142,10 @@ public CircularBuffer(int capacity, T[] items) _end = Size == capacity ? 0 : Size; } - #region IEnumerable implementation +#region IEnumerable implementation /// - /// Returns an enumerator that iterates through this buffer. + /// Returns an enumerator that iterates through this buffer. /// /// An enumerator that can be used to iterate this collection. public IEnumerator GetEnumerator() @@ -163,19 +161,17 @@ public IEnumerator GetEnumerator() } } - #endregion +#endregion - #region IEnumerable implementation +#region IEnumerable implementation IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } + => GetEnumerator(); - #endregion +#endregion /// - /// Element at the back of the buffer - this[Size - 1]. + /// Element at the back of the buffer - this[Size - 1]. /// /// The value of the element of type T at the back of the buffer. public T Back() @@ -186,7 +182,7 @@ public T Back() } /// - /// Clears the contents of the array. Size = 0, Capacity is unchanged. + /// Clears the contents of the array. Size = 0, Capacity is unchanged. /// /// public void Clear() @@ -199,7 +195,7 @@ public void Clear() } /// - /// Element at the front of the buffer - this[0]. + /// Element at the front of the buffer - this[0]. /// /// The value of the element of type T at the front of the buffer. public T Front() @@ -210,8 +206,8 @@ public T Front() } /// - /// Removes the element at the back of the buffer. Decreasing the - /// Buffer size by 1. + /// Removes the element at the back of the buffer. Decreasing the + /// Buffer size by 1. /// public void PopBack() { @@ -222,8 +218,8 @@ public void PopBack() } /// - /// Removes the element at the front of the buffer. Decreasing the - /// Buffer size by 1. + /// Removes the element at the front of the buffer. Decreasing the + /// Buffer size by 1. /// public void PopFront() { @@ -234,10 +230,10 @@ public void PopFront() } /// - /// Pushes a new element to the back of the buffer. Back()/this[Size-1] - /// will now return this element. - /// When the buffer is full, the element at Front()/this[0] will be - /// popped to allow for this new element to fit. + /// Pushes a new element to the back of the buffer. Back()/this[Size-1] + /// will now return this element. + /// When the buffer is full, the element at Front()/this[0] will be + /// popped to allow for this new element to fit. /// /// Item to push to the back of the buffer public void PushBack(T item) @@ -257,8 +253,8 @@ public void PushBack(T item) } /// - /// Pushes a contiguous range of elements to the back of the buffer in bulk. - /// When the buffer is full, the oldest elements are dropped to make room. + /// Pushes a contiguous range of elements to the back of the buffer in bulk. + /// When the buffer is full, the oldest elements are dropped to make room. /// /// Items to push. public void PushBackRange(ReadOnlySpan items) @@ -317,10 +313,10 @@ public void PushBackRange(ReadOnlySpan items) } /// - /// Pushes a new element to the front of the buffer. Front()/this[0] - /// will now return this element. - /// When the buffer is full, the element at Back()/this[Size-1] will be - /// popped to allow for this new element to fit. + /// Pushes a new element to the front of the buffer. Front()/this[0] + /// will now return this element. + /// When the buffer is full, the element at Back()/this[Size-1] will be + /// popped to allow for this new element to fit. /// /// Item to push to the front of the buffer public void PushFront(T item) @@ -340,9 +336,9 @@ public void PushFront(T item) } /// - /// Copies the buffer contents to an array, according to the logical - /// contents of the buffer (i.e. independent of the internal - /// order/contents) + /// Copies the buffer contents to an array, according to the logical + /// contents of the buffer (i.e. independent of the internal + /// order/contents) /// /// A new array with a copy of the buffer contents. public T[] ToArray() @@ -361,23 +357,21 @@ public T[] ToArray() } /// - /// Get the contents of the buffer as 2 ArraySegments. - /// Respects the logical contents of the buffer, where - /// each segment and items in each segment are ordered - /// according to insertion. - /// Fast: does not copy the array elements. - /// Useful for methods like Send(IList<ArraySegment<Byte>>). - /// Segments may be empty. + /// Get the contents of the buffer as 2 ArraySegments. + /// Respects the logical contents of the buffer, where + /// each segment and items in each segment are ordered + /// according to insertion. + /// Fast: does not copy the array elements. + /// Useful for methods like Send(IList<ArraySegment<Byte>>). + /// Segments may be empty. /// /// An IList with 2 segments corresponding to the buffer content. public IList> ToArraySegments() - { - return [ArrayOne(), ArrayTwo()]; - } + => [ArrayOne(), ArrayTwo()]; /// - /// Decrements the provided index variable by one, wrapping - /// around if necessary. + /// Decrements the provided index variable by one, wrapping + /// around if necessary. /// /// private void Decrement(ref int index) @@ -391,8 +385,8 @@ private void Decrement(ref int index) } /// - /// Increments the provided index variable by one, wrapping - /// around if necessary. + /// Increments the provided index variable by one, wrapping + /// around if necessary. /// /// private void Increment(ref int index) @@ -404,18 +398,16 @@ private void Increment(ref int index) } /// - /// Converts the index in the argument to an index in _buffer + /// Converts the index in the argument to an index in _buffer /// /// - /// The transformed index. + /// The transformed index. /// /// - /// External index. + /// External index. /// private int InternalIndex(int index) - { - return _start + (index < Capacity - _start ? index : index - Capacity); - } + => _start + (index < Capacity - _start ? index : index - Capacity); private void ThrowIfEmpty(string message = "Cannot access an empty buffer.") { @@ -430,7 +422,7 @@ private void ThrowIfEmpty(string message = "Cannot access an empty buffer.") // http://www.boost.org/doc/libs/1_37_0/libs/circular_buffer/doc/circular_buffer.html#classboost_1_1circular__buffer_1f5081a54afbc2dfc1a7fb20329df7d5b // should help a lot with the code. - #region Array items easy access. +#region Array items easy access. // The array is composed by at most two non-contiguous segments, // the next two methods allow easy access to those. @@ -439,31 +431,31 @@ private ArraySegment ArrayOne() { if (IsEmpty) { - return new ArraySegment([]); + return new([]); } if (_start < _end) { - return new ArraySegment(_buffer, _start, _end - _start); + return new(_buffer, _start, _end - _start); } - return new ArraySegment(_buffer, _start, _buffer.Length - _start); + return new(_buffer, _start, _buffer.Length - _start); } private ArraySegment ArrayTwo() { if (IsEmpty) { - return new ArraySegment([]); + return new([]); } if (_start < _end) { - return new ArraySegment(_buffer, _end, 0); + return new(_buffer, _end, 0); } - return new ArraySegment(_buffer, 0, _end); + return new(_buffer, 0, _end); } - #endregion +#endregion } diff --git a/src/SquidStd.Network/Client/SquidStdTcpClient.cs b/src/SquidStd.Network/Client/SquidStdTcpClient.cs index 5bf29931..7b7f9705 100644 --- a/src/SquidStd.Network/Client/SquidStdTcpClient.cs +++ b/src/SquidStd.Network/Client/SquidStdTcpClient.cs @@ -13,8 +13,8 @@ namespace SquidStd.Network.Client; /// -/// Represents a connected TCP client with async send/receive loops, -/// middleware processing, lifecycle events, and recent byte history. +/// Represents a connected TCP client with async send/receive loops, +/// middleware processing, lifecycle events, and recent byte history. /// public sealed class SquidStdTcpClient : INetworkConnection, IAsyncDisposable, IDisposable { @@ -44,12 +44,12 @@ public sealed class SquidStdTcpClient : INetworkConnection, IAsyncDisposable, ID private int _started; /// - /// Receives payload chunk size in bytes. + /// Receives payload chunk size in bytes. /// public int ReceiveBufferSize { get; } /// - /// Local endpoint used for this connection, when available. + /// Local endpoint used for this connection, when available. /// public EndPoint? LocalEndPoint { @@ -67,7 +67,7 @@ public EndPoint? LocalEndPoint } /// - /// Gets the number of bytes currently available in the receive circular buffer. + /// Gets the number of bytes currently available in the receive circular buffer. /// public int AvailableBytes { @@ -81,7 +81,7 @@ public int AvailableBytes } /// - /// Gets whether the receive circular buffer is full. + /// Gets whether the receive circular buffer is full. /// public bool IsReceiveBufferFull { @@ -95,12 +95,12 @@ public bool IsReceiveBufferFull } /// - /// Unique session identifier for this client connection. + /// Unique session identifier for this client connection. /// public long SessionId { get; } /// - /// Client remote endpoint, when connected. + /// Client remote endpoint, when connected. /// public EndPoint? RemoteEndPoint { @@ -118,18 +118,18 @@ public EndPoint? RemoteEndPoint } /// - /// True when the underlying socket is connected and client not closed. + /// True when the underlying socket is connected and client not closed. /// public bool IsConnected => _socket.Connected && Volatile.Read(ref _closed) == 0; /// - /// Creates a client wrapper for an accepted socket. + /// Creates a client wrapper for an accepted socket. /// /// Connected socket. /// Optional middleware list. /// - /// Optional framer. When supplied, the receive loop accumulates middleware output and - /// emits once per complete frame instead of once per socket read. + /// Optional framer. When supplied, the receive loop accumulates middleware output and + /// emits once per complete frame instead of once per socket read. /// /// Receive chunk size in bytes. /// Max number of received bytes to keep in history. @@ -150,12 +150,10 @@ public SquidStdTcpClient( receiveBufferSize, historyBufferCapacity, maxFrameLength - ) - { - } + ) { } /// - /// Creates a client wrapper for an accepted socket using the supplied transport stream. + /// Creates a client wrapper for an accepted socket using the supplied transport stream. /// public SquidStdTcpClient( Socket socket, @@ -173,10 +171,10 @@ public SquidStdTcpClient( _socket = socket; _stream = stream; - _middlewarePipeline = new NetMiddlewarePipeline(middlewares); + _middlewarePipeline = new(middlewares); _framer = framer; _codec = codec; - _receiveBuffer = new CircularBuffer(historyBufferCapacity); + _receiveBuffer = new(historyBufferCapacity); ReceiveBufferSize = receiveBufferSize; _maxFrameLength = maxFrameLength; SessionId = Interlocked.Increment(ref _sessionIdSequence); @@ -207,7 +205,7 @@ public async ValueTask DisposeAsync() } /// - /// Closes the client connection and raises disconnect event once. + /// Closes the client connection and raises disconnect event once. /// public async Task CloseAsync(CancellationToken cancellationToken = default) { @@ -248,7 +246,7 @@ public async Task CloseAsync(CancellationToken cancellationToken = default) } /// - /// Sends a payload to the connected socket. + /// Sends a payload to the connected socket. /// public async Task SendAsync(ReadOnlyMemory payload, CancellationToken cancellationToken) { @@ -304,7 +302,7 @@ public async Task SendAsync(ReadOnlyMemory payload, CancellationToken canc } /// - /// Adds a middleware component to this client pipeline. + /// Adds a middleware component to this client pipeline. /// public SquidStdTcpClient AddMiddleware(INetMiddleware middleware) { @@ -314,7 +312,7 @@ public SquidStdTcpClient AddMiddleware(INetMiddleware middleware) } /// - /// Creates an outbound client and connects to the specified endpoint. + /// Creates an outbound client and connects to the specified endpoint. /// public static async Task ConnectAsync( IPEndPoint endPoint, @@ -334,7 +332,7 @@ public static async Task ConnectAsync( } /// - /// Consumes bytes from the front of the receive circular buffer. + /// Consumes bytes from the front of the receive circular buffer. /// public int ConsumeBytes(int count) { @@ -357,24 +355,20 @@ public int ConsumeBytes(int count) } /// - /// Checks whether this client pipeline contains at least one middleware instance of the specified type. + /// Checks whether this client pipeline contains at least one middleware instance of the specified type. /// public bool ContainsMiddleware() where TMiddleware : INetMiddleware - { - return _middlewarePipeline.ContainsMiddleware(); - } + => _middlewarePipeline.ContainsMiddleware(); /// - /// Returns a snapshot of recent received bytes from the circular history buffer. + /// Returns a snapshot of recent received bytes from the circular history buffer. /// public byte[] GetRecentReceivedBytes() - { - return PeekData(); - } + => PeekData(); /// - /// Peeks at data in the receive circular buffer without consuming it. + /// Peeks at data in the receive circular buffer without consuming it. /// public byte[] PeekData(int count = 0) { @@ -398,26 +392,22 @@ public byte[] PeekData(int count = 0) } /// - /// Atomically swaps the transport codec for this connection. The new codec takes effect from the next - /// socket read; the caller must trigger the swap at a read boundary (no old-regime bytes still pending). + /// Atomically swaps the transport codec for this connection. The new codec takes effect from the next + /// socket read; the caller must trigger the swap at a read boundary (no old-regime bytes still pending). /// /// The new codec, or null to remove transport transformation. public void SwapCodec(ITransportCodec? codec) - { - Volatile.Write(ref _codec, codec); - } + => Volatile.Write(ref _codec, codec); /// - /// Removes all middleware components of the specified type from this client pipeline. + /// Removes all middleware components of the specified type from this client pipeline. /// public bool RemoveMiddleware() where TMiddleware : INetMiddleware - { - return _middlewarePipeline.RemoveMiddleware(); - } + => _middlewarePipeline.RemoveMiddleware(); /// - /// Starts the receive loop and raises connect event. + /// Starts the receive loop and raises connect event. /// public Task StartAsync(CancellationToken cancellationToken) { @@ -495,9 +485,7 @@ private void EmitFrames() // receive loop close the connection before the buffer grows further. if (_pendingLength > _maxFrameLength) { - throw new InvalidDataException( - $"Incoming frame exceeds the maximum of {_maxFrameLength} bytes." - ); + throw new InvalidDataException($"Incoming frame exceeds the maximum of {_maxFrameLength} bytes."); } break; @@ -524,7 +512,7 @@ private void EmitFrames() ConsumePending(frameLength); - OnDataReceived?.Invoke(this, new SquidStdTcpDataReceivedEventArgs(this, frame)); + OnDataReceived?.Invoke(this, new(this, frame)); } } @@ -535,7 +523,7 @@ private void RaiseConnected() SessionId, RemoteEndPoint ); - OnConnected?.Invoke(this, new SquidStdTcpClientEventArgs(this)); + OnConnected?.Invoke(this, new(this)); } private void RaiseDisconnected() @@ -545,7 +533,7 @@ private void RaiseDisconnected() SessionId, RemoteEndPoint ); - OnDisconnected?.Invoke(this, new SquidStdTcpClientEventArgs(this)); + OnDisconnected?.Invoke(this, new(this)); } private void RaiseException(Exception exception) @@ -556,7 +544,7 @@ private void RaiseException(Exception exception) SessionId, RemoteEndPoint ); - OnException?.Invoke(this, new SquidStdTcpExceptionEventArgs(exception, this)); + OnException?.Invoke(this, new(exception, this)); } private async Task ReceiveLoopAsync() @@ -568,9 +556,9 @@ private async Task ReceiveLoopAsync() while (!_internalCancellationTokenSource.IsCancellationRequested && IsConnected) { var received = await _stream.ReadAsync( - buffer.AsMemory(0, ReceiveBufferSize), - _internalCancellationTokenSource.Token - ); + buffer.AsMemory(0, ReceiveBufferSize), + _internalCancellationTokenSource.Token + ); if (received <= 0) { @@ -592,10 +580,10 @@ private async Task ReceiveLoopAsync() var chunkMemory = new ReadOnlyMemory(chunk, 0, received); var processed = await _middlewarePipeline.ExecuteAsync( - this, - chunkMemory, - _internalCancellationTokenSource.Token - ); + this, + chunkMemory, + _internalCancellationTokenSource.Token + ); if (processed.IsEmpty) { @@ -607,7 +595,7 @@ private async Task ReceiveLoopAsync() // Fresh copy so the event handler can outlive the pooled chunk. var payload = new byte[processed.Length]; processed.CopyTo(payload); - OnDataReceived?.Invoke(this, new SquidStdTcpDataReceivedEventArgs(this, payload)); + OnDataReceived?.Invoke(this, new(this, payload)); } else { @@ -652,27 +640,25 @@ private void ReleasePendingBuffer() /// public void Dispose() // Sync-over-async: best effort. Prefer DisposeAsync. - { - DisposeAsync().AsTask().GetAwaiter().GetResult(); - } + => DisposeAsync().AsTask().GetAwaiter().GetResult(); /// - /// Raised when the client is fully connected and receive loop starts. + /// Raised when the client is fully connected and receive loop starts. /// public event EventHandler? OnConnected; /// - /// Raised when the client is disconnected. + /// Raised when the client is disconnected. /// public event EventHandler? OnDisconnected; /// - /// Raised when data is received (after middleware pipeline). + /// Raised when data is received (after middleware pipeline). /// public event EventHandler? OnDataReceived; /// - /// Raised when receive/send loops throw an exception. + /// Raised when receive/send loops throw an exception. /// public event EventHandler? OnException; } diff --git a/src/SquidStd.Network/Client/SquidStdUdpClient.cs b/src/SquidStd.Network/Client/SquidStdUdpClient.cs index 2918aaa4..de8d31ee 100644 --- a/src/SquidStd.Network/Client/SquidStdUdpClient.cs +++ b/src/SquidStd.Network/Client/SquidStdUdpClient.cs @@ -7,11 +7,11 @@ namespace SquidStd.Network.Client; /// -/// Connectionless UDP client that binds a local socket and surfaces inbound datagrams through an -/// async receive loop. Datagrams can be sent to any endpoint with , or to -/// an optional default remote endpoint via . Mirrors the lifecycle surface of -/// (session id, connect/disconnect/data/exception events, and -/// ). Supports Start once; recreate the instance to listen again. +/// Connectionless UDP client that binds a local socket and surfaces inbound datagrams through an +/// async receive loop. Datagrams can be sent to any endpoint with , or to +/// an optional default remote endpoint via . Mirrors the lifecycle surface of +/// (session id, connect/disconnect/data/exception events, and +/// ). Supports Start once; recreate the instance to listen again. /// public sealed class SquidStdUdpClient : INetworkConnection, IAsyncDisposable, IDisposable { @@ -27,7 +27,7 @@ public sealed class SquidStdUdpClient : INetworkConnection, IAsyncDisposable, ID private int _started; /// - /// Local endpoint the client is bound to, when available. + /// Local endpoint the client is bound to, when available. /// public EndPoint? LocalEndPoint { @@ -45,33 +45,33 @@ public EndPoint? LocalEndPoint } /// - /// Unique session identifier for this client. + /// Unique session identifier for this client. /// public long SessionId { get; } /// - /// Default remote endpoint used by , when configured. + /// Default remote endpoint used by , when configured. /// public EndPoint? RemoteEndPoint => _defaultRemoteEndPoint; /// - /// True while the client is open (not closed). + /// True while the client is open (not closed). /// public bool IsConnected => Volatile.Read(ref _closed) == 0; /// - /// Creates a UDP client bound to a local endpoint. + /// Creates a UDP client bound to a local endpoint. /// /// - /// Local endpoint to bind. When null, binds an ephemeral port on . + /// Local endpoint to bind. When null, binds an ephemeral port on . /// /// - /// Optional default destination used by . When null, callers must use - /// with an explicit endpoint. + /// Optional default destination used by . When null, callers must use + /// with an explicit endpoint. /// public SquidStdUdpClient(IPEndPoint? localEndPoint = null, IPEndPoint? defaultRemoteEndPoint = null) { - _udpClient = new UdpClient(localEndPoint ?? new IPEndPoint(IPAddress.Any, 0)); + _udpClient = new(localEndPoint ?? new IPEndPoint(IPAddress.Any, 0)); _defaultRemoteEndPoint = defaultRemoteEndPoint; SessionId = Interlocked.Increment(ref _sessionIdSequence); } @@ -99,7 +99,7 @@ public async ValueTask DisposeAsync() } /// - /// Closes the client and raises once. + /// Closes the client and raises once. /// public async Task CloseAsync(CancellationToken cancellationToken = default) { @@ -123,7 +123,7 @@ public async Task CloseAsync(CancellationToken cancellationToken = default) } /// - /// Sends a datagram to the configured default remote endpoint. + /// Sends a datagram to the configured default remote endpoint. /// /// No default remote endpoint was configured. public Task SendAsync(ReadOnlyMemory payload, CancellationToken cancellationToken) @@ -139,7 +139,7 @@ public Task SendAsync(ReadOnlyMemory payload, CancellationToken cancellati } /// - /// Sends a datagram to an explicit endpoint. + /// Sends a datagram to an explicit endpoint. /// public async Task SendToAsync(ReadOnlyMemory payload, IPEndPoint endPoint, CancellationToken cancellationToken) { @@ -161,7 +161,7 @@ public async Task SendToAsync(ReadOnlyMemory payload, IPEndPoint endPoint, } /// - /// Starts the receive loop and raises . + /// Starts the receive loop and raises . /// public Task StartAsync(CancellationToken cancellationToken) { @@ -189,7 +189,7 @@ private void RaiseConnected() SessionId, LocalEndPoint ); - OnConnected?.Invoke(this, new SquidStdUdpClientEventArgs(this)); + OnConnected?.Invoke(this, new(this)); } private void RaiseDisconnected() @@ -199,13 +199,13 @@ private void RaiseDisconnected() SessionId, LocalEndPoint ); - OnDisconnected?.Invoke(this, new SquidStdUdpClientEventArgs(this)); + OnDisconnected?.Invoke(this, new(this)); } private void RaiseException(Exception exception) { _logger.Error(exception, "UDP client exception. SessionId={SessionId}", SessionId); - OnException?.Invoke(this, new SquidStdUdpExceptionEventArgs(exception, this)); + OnException?.Invoke(this, new(exception, this)); } private async Task ReceiveLoopAsync() @@ -219,7 +219,7 @@ private async Task ReceiveLoopAsync() // UdpReceiveResult.Buffer is a fresh array per receive, so it is safe to hand off. OnDataReceived?.Invoke( this, - new SquidStdUdpDataReceivedEventArgs(this, result.RemoteEndPoint, result.Buffer) + new(this, result.RemoteEndPoint, result.Buffer) ); } } @@ -248,27 +248,25 @@ private async Task ReceiveLoopAsync() /// public void Dispose() // Sync-over-async: best effort. Prefer DisposeAsync. - { - DisposeAsync().AsTask().GetAwaiter().GetResult(); - } + => DisposeAsync().AsTask().GetAwaiter().GetResult(); /// - /// Raised when the client starts and the receive loop begins. + /// Raised when the client starts and the receive loop begins. /// public event EventHandler? OnConnected; /// - /// Raised when the client is closed. + /// Raised when the client is closed. /// public event EventHandler? OnDisconnected; /// - /// Raised once per received datagram. + /// Raised once per received datagram. /// public event EventHandler? OnDataReceived; /// - /// Raised when the receive/send paths throw an unexpected exception. + /// Raised when the receive/send paths throw an unexpected exception. /// public event EventHandler? OnException; } diff --git a/src/SquidStd.Network/Data/ConnectionPipeline.cs b/src/SquidStd.Network/Data/ConnectionPipeline.cs index 15b13650..809a3932 100644 --- a/src/SquidStd.Network/Data/ConnectionPipeline.cs +++ b/src/SquidStd.Network/Data/ConnectionPipeline.cs @@ -5,8 +5,8 @@ namespace SquidStd.Network.Data; /// -/// Per-connection transport configuration produced by a server factory on each accepted connection. -/// Any member left null falls back to the server's shared configuration. +/// Per-connection transport configuration produced by a server factory on each accepted connection. +/// Any member left null falls back to the server's shared configuration. /// public sealed record ConnectionPipeline( ITransportCodec? Codec = null, diff --git a/src/SquidStd.Network/Data/Events/SquidStdSessionDataEventArgs.cs b/src/SquidStd.Network/Data/Events/SquidStdSessionDataEventArgs.cs index 2c39018d..4c8273ef 100644 --- a/src/SquidStd.Network/Data/Events/SquidStdSessionDataEventArgs.cs +++ b/src/SquidStd.Network/Data/Events/SquidStdSessionDataEventArgs.cs @@ -3,7 +3,7 @@ namespace SquidStd.Network.Data.Events; /// -/// Event payload carrying a session and the data it received. +/// Event payload carrying a session and the data it received. /// public sealed class SquidStdSessionDataEventArgs : EventArgs { diff --git a/src/SquidStd.Network/Data/Events/SquidStdSessionEventArgs.cs b/src/SquidStd.Network/Data/Events/SquidStdSessionEventArgs.cs index 4c8ef775..65fc0233 100644 --- a/src/SquidStd.Network/Data/Events/SquidStdSessionEventArgs.cs +++ b/src/SquidStd.Network/Data/Events/SquidStdSessionEventArgs.cs @@ -3,7 +3,7 @@ namespace SquidStd.Network.Data.Events; /// -/// Event payload carrying a session (created or removed). +/// Event payload carrying a session (created or removed). /// public sealed class SquidStdSessionEventArgs : EventArgs { diff --git a/src/SquidStd.Network/Data/Events/SquidStdTcpClientEventArgs.cs b/src/SquidStd.Network/Data/Events/SquidStdTcpClientEventArgs.cs index 56c64ae3..31e1920f 100644 --- a/src/SquidStd.Network/Data/Events/SquidStdTcpClientEventArgs.cs +++ b/src/SquidStd.Network/Data/Events/SquidStdTcpClientEventArgs.cs @@ -3,12 +3,12 @@ namespace SquidStd.Network.Data.Events; /// -/// Event payload containing a network client instance. +/// Event payload containing a network client instance. /// public sealed class SquidStdTcpClientEventArgs : EventArgs { /// - /// Connected or disconnected client. + /// Connected or disconnected client. /// public SquidStdTcpClient Client { get; } diff --git a/src/SquidStd.Network/Data/Events/SquidStdTcpDataReceivedEventArgs.cs b/src/SquidStd.Network/Data/Events/SquidStdTcpDataReceivedEventArgs.cs index 729e2aef..3fec6f48 100644 --- a/src/SquidStd.Network/Data/Events/SquidStdTcpDataReceivedEventArgs.cs +++ b/src/SquidStd.Network/Data/Events/SquidStdTcpDataReceivedEventArgs.cs @@ -3,17 +3,17 @@ namespace SquidStd.Network.Data.Events; /// -/// Event payload containing data received from a network client. +/// Event payload containing data received from a network client. /// public sealed class SquidStdTcpDataReceivedEventArgs : EventArgs { /// - /// Source client for the data payload. + /// Source client for the data payload. /// public SquidStdTcpClient Client { get; } /// - /// Received data payload. + /// Received data payload. /// public ReadOnlyMemory Data { get; } diff --git a/src/SquidStd.Network/Data/Events/SquidStdTcpExceptionEventArgs.cs b/src/SquidStd.Network/Data/Events/SquidStdTcpExceptionEventArgs.cs index 883f3a92..492be239 100644 --- a/src/SquidStd.Network/Data/Events/SquidStdTcpExceptionEventArgs.cs +++ b/src/SquidStd.Network/Data/Events/SquidStdTcpExceptionEventArgs.cs @@ -3,17 +3,17 @@ namespace SquidStd.Network.Data.Events; /// -/// Event payload containing an exception raised by server or client network loops. +/// Event payload containing an exception raised by server or client network loops. /// public sealed class SquidStdTcpExceptionEventArgs : EventArgs { /// - /// Exception raised by the networking component. + /// Exception raised by the networking component. /// public Exception Exception { get; } /// - /// Client related to the exception, when available. + /// Client related to the exception, when available. /// public SquidStdTcpClient? Client { get; } diff --git a/src/SquidStd.Network/Data/Events/SquidStdUdpClientEventArgs.cs b/src/SquidStd.Network/Data/Events/SquidStdUdpClientEventArgs.cs index 7f045360..71dbc282 100644 --- a/src/SquidStd.Network/Data/Events/SquidStdUdpClientEventArgs.cs +++ b/src/SquidStd.Network/Data/Events/SquidStdUdpClientEventArgs.cs @@ -3,12 +3,12 @@ namespace SquidStd.Network.Data.Events; /// -/// Event payload containing a UDP client instance. +/// Event payload containing a UDP client instance. /// public sealed class SquidStdUdpClientEventArgs : EventArgs { /// - /// Started or closed UDP client. + /// Started or closed UDP client. /// public SquidStdUdpClient Client { get; } diff --git a/src/SquidStd.Network/Data/Events/SquidStdUdpDataReceivedEventArgs.cs b/src/SquidStd.Network/Data/Events/SquidStdUdpDataReceivedEventArgs.cs index 4f8d526c..b931359e 100644 --- a/src/SquidStd.Network/Data/Events/SquidStdUdpDataReceivedEventArgs.cs +++ b/src/SquidStd.Network/Data/Events/SquidStdUdpDataReceivedEventArgs.cs @@ -4,22 +4,22 @@ namespace SquidStd.Network.Data.Events; /// -/// Event payload containing a datagram received from a UDP peer. +/// Event payload containing a datagram received from a UDP peer. /// public sealed class SquidStdUdpDataReceivedEventArgs : EventArgs { /// - /// UDP client that received the datagram. + /// UDP client that received the datagram. /// public SquidStdUdpClient Client { get; } /// - /// Endpoint that sent the datagram. + /// Endpoint that sent the datagram. /// public IPEndPoint RemoteEndPoint { get; } /// - /// Received datagram payload. + /// Received datagram payload. /// public ReadOnlyMemory Data { get; } diff --git a/src/SquidStd.Network/Data/Events/SquidStdUdpDatagramReceivedEventArgs.cs b/src/SquidStd.Network/Data/Events/SquidStdUdpDatagramReceivedEventArgs.cs index 56db7c5d..2cd200f2 100644 --- a/src/SquidStd.Network/Data/Events/SquidStdUdpDatagramReceivedEventArgs.cs +++ b/src/SquidStd.Network/Data/Events/SquidStdUdpDatagramReceivedEventArgs.cs @@ -3,7 +3,7 @@ namespace SquidStd.Network.Data.Events; /// -/// Event payload for a datagram received by the UDP server, carrying the sender endpoint. +/// Event payload for a datagram received by the UDP server, carrying the sender endpoint. /// public sealed class SquidStdUdpDatagramReceivedEventArgs : EventArgs { diff --git a/src/SquidStd.Network/Data/Events/SquidStdUdpExceptionEventArgs.cs b/src/SquidStd.Network/Data/Events/SquidStdUdpExceptionEventArgs.cs index 26c3f4d8..e22e173d 100644 --- a/src/SquidStd.Network/Data/Events/SquidStdUdpExceptionEventArgs.cs +++ b/src/SquidStd.Network/Data/Events/SquidStdUdpExceptionEventArgs.cs @@ -3,17 +3,17 @@ namespace SquidStd.Network.Data.Events; /// -/// Event payload containing an exception raised by the UDP client receive/send paths. +/// Event payload containing an exception raised by the UDP client receive/send paths. /// public sealed class SquidStdUdpExceptionEventArgs : EventArgs { /// - /// Exception raised by the networking component. + /// Exception raised by the networking component. /// public Exception Exception { get; } /// - /// UDP client related to the exception, when available. + /// UDP client related to the exception, when available. /// public SquidStdUdpClient? Client { get; } diff --git a/src/SquidStd.Network/Data/Options/SquidStdTcpServerTlsOptions.cs b/src/SquidStd.Network/Data/Options/SquidStdTcpServerTlsOptions.cs index 53e5a148..97d547e6 100644 --- a/src/SquidStd.Network/Data/Options/SquidStdTcpServerTlsOptions.cs +++ b/src/SquidStd.Network/Data/Options/SquidStdTcpServerTlsOptions.cs @@ -22,15 +22,13 @@ public SquidStdTcpServerTlsOptions(X509Certificate2 serverCertificate) } internal SslServerAuthenticationOptions ToAuthenticationOptions() - { - return new SslServerAuthenticationOptions + => new() { ServerCertificate = ServerCertificate, ClientCertificateRequired = ClientCertificateRequired, CertificateRevocationCheckMode = CheckCertificateRevocation - ? X509RevocationMode.Online - : X509RevocationMode.NoCheck, + ? X509RevocationMode.Online + : X509RevocationMode.NoCheck, EnabledSslProtocols = EnabledSslProtocols }; - } } diff --git a/src/SquidStd.Network/Exceptions/Buffers/CircularBufferEmptyException.cs b/src/SquidStd.Network/Exceptions/Buffers/CircularBufferEmptyException.cs index 1c50e2f3..22058ccd 100644 --- a/src/SquidStd.Network/Exceptions/Buffers/CircularBufferEmptyException.cs +++ b/src/SquidStd.Network/Exceptions/Buffers/CircularBufferEmptyException.cs @@ -1,12 +1,10 @@ namespace SquidStd.Network.Exceptions.Buffers; /// -/// Exception thrown when an operation requires at least one element in the circular buffer. +/// Exception thrown when an operation requires at least one element in the circular buffer. /// public sealed class CircularBufferEmptyException : InvalidOperationException { public CircularBufferEmptyException(string? message = null) - : base(message ?? "Circular buffer is empty.") - { - } + : base(message ?? "Circular buffer is empty.") { } } diff --git a/src/SquidStd.Network/Exceptions/Buffers/CircularBufferIndexOutOfRangeException.cs b/src/SquidStd.Network/Exceptions/Buffers/CircularBufferIndexOutOfRangeException.cs index 3df75eed..f9ce6d6e 100644 --- a/src/SquidStd.Network/Exceptions/Buffers/CircularBufferIndexOutOfRangeException.cs +++ b/src/SquidStd.Network/Exceptions/Buffers/CircularBufferIndexOutOfRangeException.cs @@ -1,12 +1,10 @@ namespace SquidStd.Network.Exceptions.Buffers; /// -/// Exception thrown when an index is outside the valid circular buffer bounds. +/// Exception thrown when an index is outside the valid circular buffer bounds. /// public sealed class CircularBufferIndexOutOfRangeException : ArgumentOutOfRangeException { public CircularBufferIndexOutOfRangeException(string paramName, int index, int size) - : base(paramName, index, $"Cannot access index {index}. Buffer size is {size}.") - { - } + : base(paramName, index, $"Cannot access index {index}. Buffer size is {size}.") { } } diff --git a/src/SquidStd.Network/Extensions/PacketExtensions.cs b/src/SquidStd.Network/Extensions/PacketExtensions.cs index 4359af89..79baf9ad 100644 --- a/src/SquidStd.Network/Extensions/PacketExtensions.cs +++ b/src/SquidStd.Network/Extensions/PacketExtensions.cs @@ -5,8 +5,6 @@ public static class PacketExtensions extension(byte opCode) { public string ToPacketString() - { - return "0x" + opCode.ToString("X2"); - } + => "0x" + opCode.ToString("X2"); } } diff --git a/src/SquidStd.Network/Interfaces/Client/INetworkConnection.cs b/src/SquidStd.Network/Interfaces/Client/INetworkConnection.cs index a0bb6858..2911ac6a 100644 --- a/src/SquidStd.Network/Interfaces/Client/INetworkConnection.cs +++ b/src/SquidStd.Network/Interfaces/Client/INetworkConnection.cs @@ -3,34 +3,34 @@ namespace SquidStd.Network.Interfaces.Client; /// -/// Represents a connected network client independently from the underlying transport. +/// Represents a connected network client independently from the underlying transport. /// public interface INetworkConnection { /// - /// Unique connection identifier assigned by the transport. + /// Unique connection identifier assigned by the transport. /// long SessionId { get; } /// - /// Remote endpoint when the transport exposes one. + /// Remote endpoint when the transport exposes one. /// EndPoint? RemoteEndPoint { get; } /// - /// Indicates whether the connection is still open. + /// Indicates whether the connection is still open. /// bool IsConnected { get; } /// - /// Closes the connection. + /// Closes the connection. /// /// The cancellation token. /// A task that completes when the connection has closed. Task CloseAsync(CancellationToken cancellationToken = default); /// - /// Sends raw bytes to the connected client. + /// Sends raw bytes to the connected client. /// /// The payload bytes. /// The cancellation token. diff --git a/src/SquidStd.Network/Interfaces/Codecs/ITransportCodec.cs b/src/SquidStd.Network/Interfaces/Codecs/ITransportCodec.cs index 11386dd7..4e6a60bb 100644 --- a/src/SquidStd.Network/Interfaces/Codecs/ITransportCodec.cs +++ b/src/SquidStd.Network/Interfaces/Codecs/ITransportCodec.cs @@ -1,13 +1,13 @@ namespace SquidStd.Network.Interfaces.Codecs; /// -/// An in-place, length-preserving transport transform applied at the wire — for example a stream cipher. +/// An in-place, length-preserving transport transform applied at the wire — for example a stream cipher. /// /// -/// Implementations MUST preserve the buffer length and transform in place. is invoked -/// only from a connection's receive loop (serial), and only from its send path (serial -/// under the send lock). The two directions may run concurrently (one each), so an implementation MUST NOT -/// share mutable state between decode and encode (real ciphers keep separate receive/send positions). +/// Implementations MUST preserve the buffer length and transform in place. is invoked +/// only from a connection's receive loop (serial), and only from its send path (serial +/// under the send lock). The two directions may run concurrently (one each), so an implementation MUST NOT +/// share mutable state between decode and encode (real ciphers keep separate receive/send positions). /// public interface ITransportCodec { diff --git a/src/SquidStd.Network/Interfaces/Framing/INetFramer.cs b/src/SquidStd.Network/Interfaces/Framing/INetFramer.cs index 977b4583..ff90d6db 100644 --- a/src/SquidStd.Network/Interfaces/Framing/INetFramer.cs +++ b/src/SquidStd.Network/Interfaces/Framing/INetFramer.cs @@ -1,29 +1,29 @@ namespace SquidStd.Network.Interfaces.Framing; /// -/// Extracts discrete frames from a continuous byte stream. +/// Extracts discrete frames from a continuous byte stream. /// /// -/// A framer is the optional bridge between the byte-oriented middleware pipeline -/// and a protocol-specific consumer. Implementations are typically stateless and -/// MUST be safe to call repeatedly: the same buffer may be inspected several times -/// as more bytes arrive across socket reads. Implementations holding per-connection -/// state must be supplied as a fresh instance per client. +/// A framer is the optional bridge between the byte-oriented middleware pipeline +/// and a protocol-specific consumer. Implementations are typically stateless and +/// MUST be safe to call repeatedly: the same buffer may be inspected several times +/// as more bytes arrive across socket reads. Implementations holding per-connection +/// state must be supplied as a fresh instance per client. /// public interface INetFramer { /// - /// Tries to read one complete frame from the start of . + /// Tries to read one complete frame from the start of . /// /// Accumulated bytes available for inspection. /// - /// The number of bytes to consume from the start of the buffer when a complete - /// frame is present. Undefined when the method returns false. + /// The number of bytes to consume from the start of the buffer when a complete + /// frame is present. Undefined when the method returns false. /// /// - /// true when starts with a complete frame and - /// has been written; false when more bytes - /// are required. + /// true when starts with a complete frame and + /// has been written; false when more bytes + /// are required. /// bool TryReadFrame(ReadOnlySpan buffer, out int frameLength); } diff --git a/src/SquidStd.Network/Interfaces/Middleware/INetMiddleware.cs b/src/SquidStd.Network/Interfaces/Middleware/INetMiddleware.cs index d5a3a047..ffc715c2 100644 --- a/src/SquidStd.Network/Interfaces/Middleware/INetMiddleware.cs +++ b/src/SquidStd.Network/Interfaces/Middleware/INetMiddleware.cs @@ -3,19 +3,19 @@ namespace SquidStd.Network.Interfaces.Middleware; /// -/// Transforms raw network bytes for a client connection. +/// Transforms raw network bytes for a client connection. /// /// -/// Middleware operates on raw bytes and MUST NOT assume any message, packet, or frame -/// semantics — framing and protocol parsing are the consumer's responsibility, applied -/// to the OnDataReceived output of the client. Returning -/// from either method drops the payload and -/// short-circuits the remaining pipeline. +/// Middleware operates on raw bytes and MUST NOT assume any message, packet, or frame +/// semantics — framing and protocol parsing are the consumer's responsibility, applied +/// to the OnDataReceived output of the client. Returning +/// from either method drops the payload and +/// short-circuits the remaining pipeline. /// public interface INetMiddleware { /// - /// Transforms an incoming payload before it is dispatched to consumers. + /// Transforms an incoming payload before it is dispatched to consumers. /// /// Client associated with the payload, if available. /// Incoming bytes. @@ -28,7 +28,7 @@ ValueTask> ProcessAsync( ); /// - /// Transforms an outgoing payload before it is written to the socket. + /// Transforms an outgoing payload before it is written to the socket. /// /// Client associated with the payload, if available. /// Outgoing bytes. @@ -39,7 +39,5 @@ ValueTask> ProcessSendAsync( ReadOnlyMemory data, CancellationToken cancellationToken = default ) - { - return ValueTask.FromResult(data); - } + => ValueTask.FromResult(data); } diff --git a/src/SquidStd.Network/Interfaces/Processing/IResultProcessor.cs b/src/SquidStd.Network/Interfaces/Processing/IResultProcessor.cs index 5d5e4562..529b9411 100644 --- a/src/SquidStd.Network/Interfaces/Processing/IResultProcessor.cs +++ b/src/SquidStd.Network/Interfaces/Processing/IResultProcessor.cs @@ -3,13 +3,13 @@ namespace SquidStd.Network.Interfaces.Processing; /// -/// Processes a framed network payload into a typed result. +/// Processes a framed network payload into a typed result. /// /// The processed result type. public interface IResultProcessor { /// - /// Processes one complete framed payload for a network connection. + /// Processes one complete framed payload for a network connection. /// /// The connection that produced the payload. /// The framed payload bytes. diff --git a/src/SquidStd.Network/Interfaces/Server/INetworkServer.cs b/src/SquidStd.Network/Interfaces/Server/INetworkServer.cs index 75506f01..36fcea8c 100644 --- a/src/SquidStd.Network/Interfaces/Server/INetworkServer.cs +++ b/src/SquidStd.Network/Interfaces/Server/INetworkServer.cs @@ -3,34 +3,34 @@ namespace SquidStd.Network.Interfaces.Server; /// -/// Represents a network listener with common lifecycle and metadata. +/// Represents a network listener with common lifecycle and metadata. /// public interface INetworkServer : IAsyncDisposable { /// - /// Transport type exposed by this server. + /// Transport type exposed by this server. /// ServerType ServerType { get; } /// - /// Current listening port. Returns 0 when no concrete port is bound. + /// Current listening port. Returns 0 when no concrete port is bound. /// int Port { get; } /// - /// Indicates whether the server is currently running. + /// Indicates whether the server is currently running. /// bool IsRunning { get; } /// - /// Starts the listener. + /// Starts the listener. /// /// The cancellation token. /// A task that completes when the listener has started. Task StartAsync(CancellationToken cancellationToken); /// - /// Stops the listener. + /// Stops the listener. /// /// The cancellation token. /// A task that completes when the listener has stopped. diff --git a/src/SquidStd.Network/Interfaces/Sessions/ISessionManager.cs b/src/SquidStd.Network/Interfaces/Sessions/ISessionManager.cs index 576e2e67..6f52a30c 100644 --- a/src/SquidStd.Network/Interfaces/Sessions/ISessionManager.cs +++ b/src/SquidStd.Network/Interfaces/Sessions/ISessionManager.cs @@ -4,7 +4,7 @@ namespace SquidStd.Network.Interfaces.Sessions; /// -/// Tracks active connections and their application state. +/// Tracks active connections and their application state. /// /// Application-defined per-connection state. public interface ISessionManager diff --git a/src/SquidStd.Network/Pipeline/NetMiddlewarePipeline.cs b/src/SquidStd.Network/Pipeline/NetMiddlewarePipeline.cs index 55afa18a..e47d9db6 100644 --- a/src/SquidStd.Network/Pipeline/NetMiddlewarePipeline.cs +++ b/src/SquidStd.Network/Pipeline/NetMiddlewarePipeline.cs @@ -4,14 +4,14 @@ namespace SquidStd.Network.Pipeline; /// -/// Executes components in registration order over a byte payload. +/// Executes components in registration order over a byte payload. /// /// -/// The pipeline is a byte transformer: each middleware sees a -/// of bytes and produces a of bytes. There is no concept of -/// message, packet, or frame at this layer — protocol-specific framing must happen on top of -/// the client's OnDataReceived output. Returning -/// from a middleware drops the payload and stops the chain. +/// The pipeline is a byte transformer: each middleware sees a +/// of bytes and produces a of bytes. There is no concept of +/// message, packet, or frame at this layer — protocol-specific framing must happen on top of +/// the client's OnDataReceived output. Returning +/// from a middleware drops the payload and stops the chain. /// public sealed class NetMiddlewarePipeline { @@ -19,7 +19,7 @@ public sealed class NetMiddlewarePipeline private INetMiddleware[] _middlewares; /// - /// Initializes the middleware pipeline. + /// Initializes the middleware pipeline. /// /// Optional initial middleware sequence. public NetMiddlewarePipeline(IEnumerable? middlewares = null) @@ -28,7 +28,7 @@ public NetMiddlewarePipeline(IEnumerable? middlewares = null) } /// - /// Adds a middleware component at the end of the execution chain. + /// Adds a middleware component at the end of the execution chain. /// /// Middleware to register. public void AddMiddleware(INetMiddleware middleware) @@ -40,7 +40,7 @@ public void AddMiddleware(INetMiddleware middleware) } /// - /// Checks whether at least one middleware component of the specified type is registered. + /// Checks whether at least one middleware component of the specified type is registered. /// /// Middleware type to check. /// true when a matching middleware is registered; otherwise false. @@ -54,7 +54,7 @@ public bool ContainsMiddleware() } /// - /// Processes the payload through all registered middleware components. + /// Processes the payload through all registered middleware components. /// /// Client associated with the payload, if available. /// Incoming payload. @@ -90,7 +90,7 @@ CancellationToken cancellationToken } /// - /// Processes the outgoing payload through all registered middleware components. + /// Processes the outgoing payload through all registered middleware components. /// /// Client associated with the payload, if available. /// Outgoing payload. @@ -126,7 +126,7 @@ CancellationToken cancellationToken } /// - /// Removes all middleware components of the specified type. + /// Removes all middleware components of the specified type. /// /// Middleware type to remove. /// true when at least one middleware was removed; otherwise false. diff --git a/src/SquidStd.Network/Server/SquidStdUdpServer.cs b/src/SquidStd.Network/Server/SquidStdUdpServer.cs index e62b9424..35da3fe7 100644 --- a/src/SquidStd.Network/Server/SquidStdUdpServer.cs +++ b/src/SquidStd.Network/Server/SquidStdUdpServer.cs @@ -10,10 +10,10 @@ namespace SquidStd.Network.Server; /// -/// Connectionless UDP server that binds one socket per local interface address and processes -/// each received datagram. By default it echoes the payload back to the sender (the behaviour the -/// UO launcher expects from a shard ping server); supply to customise the -/// response. Supports Start/Stop/Start cycles by recreating the sockets on each Start. +/// Connectionless UDP server that binds one socket per local interface address and processes +/// each received datagram. By default it echoes the payload back to the sender (the behaviour the +/// UO launcher expects from a shard ping server); supply to customise the +/// response. Supports Start/Stop/Start cycles by recreating the sockets on each Start. /// public sealed class SquidStdUdpServer : INetworkServer, IAsyncDisposable, IDisposable { @@ -29,14 +29,14 @@ public sealed class SquidStdUdpServer : INetworkServer, IAsyncDisposable, IDispo private int _started; /// - /// Optional response factory. Receives the datagram payload and the sender endpoint and returns - /// the bytes to send back; return to send no reply. - /// When null, the server echoes the payload unchanged. + /// Optional response factory. Receives the datagram payload and the sender endpoint and returns + /// the bytes to send back; return to send no reply. + /// When null, the server echoes the payload unchanged. /// public Func, IPEndPoint, ReadOnlyMemory>? OnDatagram { get; set; } /// - /// Number of bound listening sockets. + /// Number of bound listening sockets. /// public int ListenerCount { @@ -50,12 +50,12 @@ public int ListenerCount } /// - /// Transport type exposed by this server. + /// Transport type exposed by this server. /// public ServerType ServerType => ServerType.UDP; /// - /// Current listening port. Returns 0 when configured for an ephemeral port and stopped. + /// Current listening port. Returns 0 when configured for an ephemeral port and stopped. /// public int Port { @@ -76,17 +76,17 @@ public int Port } /// - /// True when the server is currently listening. + /// True when the server is currently listening. /// public bool IsRunning => Volatile.Read(ref _started) != 0; /// - /// Initializes a UDP server bound to the given endpoint on every StartAsync. + /// Initializes a UDP server bound to the given endpoint on every StartAsync. /// /// Endpoint supplying the port (and address when not binding all interfaces). /// - /// When true (default), binds one socket per local unicast address matching the endpoint's - /// address family. When false, binds only . + /// When true (default), binds one socket per local unicast address matching the endpoint's + /// address family. When false, binds only . /// public SquidStdUdpServer(IPEndPoint endPoint, bool bindAllInterfaces = true) { @@ -98,13 +98,11 @@ public SquidStdUdpServer(IPEndPoint endPoint, bool bindAllInterfaces = true) /// public async ValueTask DisposeAsync() - { - await StopAsync(CancellationToken.None); - } + => await StopAsync(CancellationToken.None); /// - /// Starts listening, binding sockets and launching a receive loop per socket. Recreates the - /// sockets on every call, so Stop/Start cycles are supported. + /// Starts listening, binding sockets and launching a receive loop per socket. Recreates the + /// sockets on every call, so Stop/Start cycles are supported. /// public Task StartAsync(CancellationToken cancellationToken) { @@ -138,7 +136,7 @@ public Task StartAsync(CancellationToken cancellationToken) } /// - /// Stops listening, closing every socket and awaiting the receive loops. + /// Stops listening, closing every socket and awaiting the receive loops. /// public async Task StopAsync(CancellationToken cancellationToken) { @@ -199,8 +197,8 @@ public async Task StopAsync(CancellationToken cancellationToken) } /// - /// Sends a datagram to a specific endpoint, using the listener that last received from it - /// (falling back to the first listener). No-op when no listener is available. + /// Sends a datagram to a specific endpoint, using the listener that last received from it + /// (falling back to the first listener). No-op when no listener is available. /// public async Task SendToAsync( IPEndPoint endPoint, @@ -230,7 +228,7 @@ public async Task SendToAsync( catch (Exception ex) { _logger.Warning(ex, "UDP SendToAsync failed for {EndPoint}", endPoint); - OnException?.Invoke(this, new SquidStdTcpExceptionEventArgs(ex)); + OnException?.Invoke(this, new(ex)); } } @@ -245,7 +243,7 @@ public async Task SendToAsync( { socket.Bind(endPoint); - return new UdpClient + return new() { Client = socket }; @@ -274,12 +272,12 @@ private async Task ReceiveLoopAsync(UdpClient listener, CancellationToken cancel _endpointListeners[result.RemoteEndPoint] = listener; OnDatagramReceived?.Invoke( this, - new SquidStdUdpDatagramReceivedEventArgs(result.RemoteEndPoint, result.Buffer) + new(result.RemoteEndPoint, result.Buffer) ); var response = OnDatagram is null - ? result.Buffer - : OnDatagram(result.Buffer, result.RemoteEndPoint); + ? result.Buffer + : OnDatagram(result.Buffer, result.RemoteEndPoint); if (!response.IsEmpty) { @@ -301,7 +299,7 @@ private async Task ReceiveLoopAsync(UdpClient listener, CancellationToken cancel catch (Exception ex) { _logger.Warning(ex, "UDP receive loop failed"); - OnException?.Invoke(this, new SquidStdTcpExceptionEventArgs(ex)); + OnException?.Invoke(this, new(ex)); } } } @@ -316,24 +314,22 @@ private IEnumerable ResolveBindEndPoints() return [ .. NetworkUtils.GetListeningAddresses(_endPoint) - .Select(address => new IPEndPoint(address.Address, _endPoint.Port)) + .Select(address => new IPEndPoint(address.Address, _endPoint.Port)) ]; } /// public void Dispose() - { - DisposeAsync().AsTask().GetAwaiter().GetResult(); - } + => DisposeAsync().AsTask().GetAwaiter().GetResult(); /// - /// Raised for every datagram received, carrying the sender endpoint. Always raised, regardless of - /// . + /// Raised for every datagram received, carrying the sender endpoint. Always raised, regardless of + /// . /// public event EventHandler? OnDatagramReceived; /// - /// Raised when receive loops throw an unexpected exception. + /// Raised when receive loops throw an unexpected exception. /// public event EventHandler? OnException; } diff --git a/src/SquidStd.Network/Server/SquidTcpServer.cs b/src/SquidStd.Network/Server/SquidTcpServer.cs index c02ab369..7411000b 100644 --- a/src/SquidStd.Network/Server/SquidTcpServer.cs +++ b/src/SquidStd.Network/Server/SquidTcpServer.cs @@ -15,8 +15,8 @@ namespace SquidStd.Network.Server; /// -/// High-throughput TCP server with client lifecycle events and middleware-enabled payload dispatch. -/// Supports Start/Stop/Start cycles by recreating the underlying socket on each Start. +/// High-throughput TCP server with client lifecycle events and middleware-enabled payload dispatch. +/// Supports Start/Stop/Start cycles by recreating the underlying socket on each Start. /// public sealed class SquidTcpServer : INetworkServer, IAsyncDisposable, IDisposable { @@ -40,34 +40,34 @@ public sealed class SquidTcpServer : INetworkServer, IAsyncDisposable, IDisposab private int _started; /// - /// Transport type exposed by this server. + /// Transport type exposed by this server. /// public ServerType ServerType => ServerType.TCP; /// - /// Current listening port. Returns 0 when the server is stopped. + /// Current listening port. Returns 0 when the server is stopped. /// public int Port => ((IPEndPoint?)_serverSocket?.LocalEndPoint)?.Port ?? 0; /// - /// True when the server is currently accepting connections. + /// True when the server is currently accepting connections. /// public bool IsRunning => Volatile.Read(ref _started) != 0; /// - /// Initializes a TCP server bound to the given endpoint. + /// Initializes a TCP server bound to the given endpoint. /// /// Endpoint to bind on every StartAsync. /// - /// Optional framer template. The same instance is shared by all accepted clients, - /// so implementations must be stateless or thread-safe. + /// Optional framer template. The same instance is shared by all accepted clients, + /// so implementations must be stateless or thread-safe. /// /// Per-client receive chunk size. /// Per-client history buffer capacity. /// - /// Optional factory invoked once per accepted connection to produce its transport configuration. - /// It MUST return fresh per-connection state — in particular a new ITransportCodec instance per - /// call — because codecs are stateful and must not be shared across connections. + /// Optional factory invoked once per accepted connection to produce its transport configuration. + /// It MUST return fresh per-connection state — in particular a new ITransportCodec instance per + /// call — because codecs are stateful and must not be shared across connections. /// public SquidTcpServer( IPEndPoint endPoint, @@ -90,13 +90,11 @@ public SquidTcpServer( /// public async ValueTask DisposeAsync() - { - await StopAsync(CancellationToken.None); - } + => await StopAsync(CancellationToken.None); /// - /// Starts accepting clients. Recreates the listening socket on every call, - /// so Stop/Start cycles are supported. + /// Starts accepting clients. Recreates the listening socket on every call, + /// so Stop/Start cycles are supported. /// public Task StartAsync(CancellationToken cancellationToken) { @@ -105,7 +103,7 @@ public Task StartAsync(CancellationToken cancellationToken) return Task.CompletedTask; } - _serverSocket = new Socket(_endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp); + _serverSocket = new(_endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp); _serverSocket.Bind(_endPoint); _serverSocket.Listen(DefaultBacklog); @@ -118,7 +116,7 @@ public Task StartAsync(CancellationToken cancellationToken) } /// - /// Stops accepting new clients and closes all active clients. + /// Stops accepting new clients and closes all active clients. /// public async Task StopAsync(CancellationToken cancellationToken) { @@ -173,7 +171,7 @@ public async Task StopAsync(CancellationToken cancellationToken) } /// - /// Registers middleware in execution order. + /// Registers middleware in execution order. /// public SquidTcpServer AddMiddleware(INetMiddleware middleware) { @@ -232,7 +230,7 @@ private async Task AcceptLoopAsync() catch (Exception ex) { _logger.Error(ex, "Accept loop failed"); - OnException?.Invoke(this, new SquidStdTcpExceptionEventArgs(ex)); + OnException?.Invoke(this, new(ex)); } } } @@ -251,10 +249,10 @@ private async Task CreateClientStreamAsync(Socket clientSocket, Cancella try { await sslStream.AuthenticateAsServerAsync( - _tlsOptions.ToAuthenticationOptions(), - cancellationToken - ) - .ConfigureAwait(false); + _tlsOptions.ToAuthenticationOptions(), + cancellationToken + ) + .ConfigureAwait(false); return sslStream; } @@ -270,67 +268,65 @@ await sslStream.AuthenticateAsServerAsync( private void WireClientEvents(SquidStdTcpClient client) { client.OnConnected += (_, args) => - { - _logger.Debug( - "OnClientConnect. SessionId={SessionId}, RemoteEndPoint={RemoteEndPoint}", - args.Client.SessionId, - args.Client.RemoteEndPoint - ); - OnClientConnect?.Invoke(this, args); - }; + { + _logger.Debug( + "OnClientConnect. SessionId={SessionId}, RemoteEndPoint={RemoteEndPoint}", + args.Client.SessionId, + args.Client.RemoteEndPoint + ); + OnClientConnect?.Invoke(this, args); + }; client.OnDataReceived += (_, args) => - { - _logger.Verbose( - "OnDataReceived. SessionId={SessionId}, Bytes={Bytes}", - args.Client.SessionId, - args.Data.Length - ); - OnDataReceived?.Invoke(this, args); - }; + { + _logger.Verbose( + "OnDataReceived. SessionId={SessionId}, Bytes={Bytes}", + args.Client.SessionId, + args.Data.Length + ); + OnDataReceived?.Invoke(this, args); + }; client.OnException += (_, args) => - { - _logger.Error( - args.Exception, - "OnException. SessionId={SessionId}", - args.Client?.SessionId - ); - OnException?.Invoke(this, args); - }; + { + _logger.Error( + args.Exception, + "OnException. SessionId={SessionId}", + args.Client?.SessionId + ); + OnException?.Invoke(this, args); + }; client.OnDisconnected += (_, args) => - { - _clients.TryRemove(args.Client.SessionId, out var _); - _logger.Debug( - "OnClientDisconnect. SessionId={SessionId}, RemoteEndPoint={RemoteEndPoint}", - args.Client.SessionId, - args.Client.RemoteEndPoint - ); - OnClientDisconnect?.Invoke(this, args); - }; + { + _clients.TryRemove(args.Client.SessionId, out var _); + _logger.Debug( + "OnClientDisconnect. SessionId={SessionId}, RemoteEndPoint={RemoteEndPoint}", + args.Client.SessionId, + args.Client.RemoteEndPoint + ); + OnClientDisconnect?.Invoke(this, args); + }; } /// public void Dispose() - { - DisposeAsync().AsTask().GetAwaiter().GetResult(); - } + => DisposeAsync().AsTask().GetAwaiter().GetResult(); /// - /// Raised when a client connects. + /// Raised when a client connects. /// public event EventHandler? OnClientConnect; /// - /// Raised when a client disconnects. + /// Raised when a client disconnects. /// public event EventHandler? OnClientDisconnect; /// - /// Raised when a client sends data after middleware processing. + /// Raised when a client sends data after middleware processing. /// public event EventHandler? OnDataReceived; /// - /// Raised when an exception happens in accept loop or client loops. + /// Raised when an exception happens in accept loop or client loops. /// public event EventHandler? OnException; } diff --git a/src/SquidStd.Network/Sessions/Session.cs b/src/SquidStd.Network/Sessions/Session.cs index 7f5584e2..6ff1a269 100644 --- a/src/SquidStd.Network/Sessions/Session.cs +++ b/src/SquidStd.Network/Sessions/Session.cs @@ -4,7 +4,7 @@ namespace SquidStd.Network.Sessions; /// -/// A tracked network connection with associated application state. +/// A tracked network connection with associated application state. /// /// Application-defined per-connection state. public sealed class Session @@ -39,13 +39,9 @@ public Session(long sessionId, INetworkConnection connection, TState state, Date /// Closes the underlying connection. public Task CloseAsync(CancellationToken cancellationToken = default) - { - return Connection.CloseAsync(cancellationToken); - } + => Connection.CloseAsync(cancellationToken); /// Sends a payload over the underlying connection. public Task SendAsync(ReadOnlyMemory payload, CancellationToken cancellationToken = default) - { - return Connection.SendAsync(payload, cancellationToken); - } + => Connection.SendAsync(payload, cancellationToken); } diff --git a/src/SquidStd.Network/Sessions/SessionManager.cs b/src/SquidStd.Network/Sessions/SessionManager.cs index 796a6244..ec66c803 100644 --- a/src/SquidStd.Network/Sessions/SessionManager.cs +++ b/src/SquidStd.Network/Sessions/SessionManager.cs @@ -8,8 +8,8 @@ namespace SquidStd.Network.Sessions; /// -/// Observes a and maintains a registry of . -/// The server is not modified; the manager subscribes to its lifecycle events. +/// Observes a and maintains a registry of . +/// The server is not modified; the manager subscribes to its lifecycle events. /// /// Application-defined per-connection state. public sealed class SessionManager : ISessionManager, IDisposable @@ -55,25 +55,19 @@ public async Task BroadcastAsync(ReadOnlyMemory payload, CancellationToken /// public Task DisconnectAsync(long sessionId, CancellationToken cancellationToken = default) - { - return _sessions.TryGetValue(sessionId, out var session) - ? session.CloseAsync(cancellationToken) - : Task.CompletedTask; - } + => _sessions.TryGetValue(sessionId, out var session) + ? session.CloseAsync(cancellationToken) + : Task.CompletedTask; /// public Task SendAsync(long sessionId, ReadOnlyMemory payload, CancellationToken cancellationToken = default) - { - return _sessions.TryGetValue(sessionId, out var session) - ? session.SendAsync(payload, cancellationToken) - : Task.CompletedTask; - } + => _sessions.TryGetValue(sessionId, out var session) + ? session.SendAsync(payload, cancellationToken) + : Task.CompletedTask; /// public bool TryGetSession(long sessionId, out Session? session) - { - return _sessions.TryGetValue(sessionId, out session); - } + => _sessions.TryGetValue(sessionId, out session); internal void HandleConnected(INetworkConnection connection) { @@ -111,25 +105,19 @@ internal void HandleDisconnected(INetworkConnection connection) } private void HandleServerClientConnect(object? sender, SquidStdTcpClientEventArgs e) - { - HandleConnected(e.Client); - } + => HandleConnected(e.Client); private void HandleServerClientDisconnect(object? sender, SquidStdTcpClientEventArgs e) - { - HandleDisconnected(e.Client); - } + => HandleDisconnected(e.Client); private void HandleServerDataReceived(object? sender, SquidStdTcpDataReceivedEventArgs e) - { - HandleData(e.Client, e.Data); - } + => HandleData(e.Client, e.Data); private void RaiseSessionCreated(Session session) { try { - OnSessionCreated?.Invoke(this, new SquidStdSessionEventArgs(session)); + OnSessionCreated?.Invoke(this, new(session)); } catch (Exception ex) { @@ -141,7 +129,7 @@ private void RaiseSessionData(Session session, ReadOnlyMemory data { try { - OnSessionData?.Invoke(this, new SquidStdSessionDataEventArgs(session, data)); + OnSessionData?.Invoke(this, new(session, data)); } catch (Exception ex) { @@ -153,7 +141,7 @@ private void RaiseSessionRemoved(Session session) { try { - OnSessionRemoved?.Invoke(this, new SquidStdSessionEventArgs(session)); + OnSessionRemoved?.Invoke(this, new(session)); } catch (Exception ex) { diff --git a/src/SquidStd.Network/Sessions/UdpSessionConnection.cs b/src/SquidStd.Network/Sessions/UdpSessionConnection.cs index b6fb7c8c..092d4b53 100644 --- a/src/SquidStd.Network/Sessions/UdpSessionConnection.cs +++ b/src/SquidStd.Network/Sessions/UdpSessionConnection.cs @@ -5,8 +5,8 @@ namespace SquidStd.Network.Sessions; /// -/// Virtual per-endpoint connection backing a UDP session. Sends route to the server's -/// ; closing removes the session via a callback. +/// Virtual per-endpoint connection backing a UDP session. Sends route to the server's +/// ; closing removes the session via a callback. /// internal sealed class UdpSessionConnection : INetworkConnection { @@ -38,7 +38,5 @@ public Task CloseAsync(CancellationToken cancellationToken = default) } public Task SendAsync(ReadOnlyMemory payload, CancellationToken cancellationToken) - { - return _server.SendToAsync(_remoteEndPoint, payload, cancellationToken); - } + => _server.SendToAsync(_remoteEndPoint, payload, cancellationToken); } diff --git a/src/SquidStd.Network/Sessions/UdpSessionEntry.cs b/src/SquidStd.Network/Sessions/UdpSessionEntry.cs index 5f5b38f0..3d4d3903 100644 --- a/src/SquidStd.Network/Sessions/UdpSessionEntry.cs +++ b/src/SquidStd.Network/Sessions/UdpSessionEntry.cs @@ -1,7 +1,7 @@ namespace SquidStd.Network.Sessions; /// -/// Internal registry entry pairing a UDP session with its last-activity timestamp. +/// Internal registry entry pairing a UDP session with its last-activity timestamp. /// /// Application-defined per-connection state. internal sealed class UdpSessionEntry diff --git a/src/SquidStd.Network/Sessions/UdpSessionManager.cs b/src/SquidStd.Network/Sessions/UdpSessionManager.cs index 4842a55e..20195e1a 100644 --- a/src/SquidStd.Network/Sessions/UdpSessionManager.cs +++ b/src/SquidStd.Network/Sessions/UdpSessionManager.cs @@ -9,8 +9,8 @@ namespace SquidStd.Network.Sessions; /// -/// Tracks per-endpoint UDP sessions over a . Sessions are created on -/// the first datagram from an endpoint and removed by idle-timeout sweep or explicit disconnect. +/// Tracks per-endpoint UDP sessions over a . Sessions are created on +/// the first datagram from an endpoint and removed by idle-timeout sweep or explicit disconnect. /// /// Application-defined per-connection state. public sealed class UdpSessionManager : ISessionManager, IDisposable @@ -75,19 +75,15 @@ public async Task BroadcastAsync(ReadOnlyMemory payload, CancellationToken /// public Task DisconnectAsync(long sessionId, CancellationToken cancellationToken = default) - { - return TryGetSession(sessionId, out var session) - ? session!.CloseAsync(cancellationToken) - : Task.CompletedTask; - } + => TryGetSession(sessionId, out var session) + ? session!.CloseAsync(cancellationToken) + : Task.CompletedTask; /// public Task SendAsync(long sessionId, ReadOnlyMemory payload, CancellationToken cancellationToken = default) - { - return TryGetSession(sessionId, out var session) - ? session!.SendAsync(payload, cancellationToken) - : Task.CompletedTask; - } + => TryGetSession(sessionId, out var session) + ? session!.SendAsync(payload, cancellationToken) + : Task.CompletedTask; /// public bool TryGetSession(long sessionId, out Session? session) @@ -106,19 +102,15 @@ public bool TryGetSession(long sessionId, out Session? session) /// Closes the session for the given endpoint. No-op when unknown. public Task DisconnectAsync(IPEndPoint endPoint, CancellationToken cancellationToken = default) - { - return TryGetSession(endPoint, out var session) - ? session!.CloseAsync(cancellationToken) - : Task.CompletedTask; - } + => TryGetSession(endPoint, out var session) + ? session!.CloseAsync(cancellationToken) + : Task.CompletedTask; /// Sends a payload to the session for the given endpoint. No-op when unknown. public Task SendToAsync(IPEndPoint endPoint, ReadOnlyMemory payload, CancellationToken cancellationToken = default) - { - return TryGetSession(endPoint, out var session) - ? session!.SendAsync(payload, cancellationToken) - : Task.CompletedTask; - } + => TryGetSession(endPoint, out var session) + ? session!.SendAsync(payload, cancellationToken) + : Task.CompletedTask; /// Looks up a session by remote endpoint. public bool TryGetSession(IPEndPoint endPoint, out Session? session) @@ -198,15 +190,13 @@ private UdpSessionEntry GetOrCreate(IPEndPoint remoteEndPoint, out bool } private void HandleServerDatagram(object? sender, SquidStdUdpDatagramReceivedEventArgs e) - { - HandleDatagram(e.RemoteEndPoint, e.Data); - } + => HandleDatagram(e.RemoteEndPoint, e.Data); private void RaiseSessionCreated(Session session) { try { - OnSessionCreated?.Invoke(this, new SquidStdSessionEventArgs(session)); + OnSessionCreated?.Invoke(this, new(session)); } catch (Exception ex) { @@ -218,7 +208,7 @@ private void RaiseSessionData(Session session, ReadOnlyMemory data { try { - OnSessionData?.Invoke(this, new SquidStdSessionDataEventArgs(session, data)); + OnSessionData?.Invoke(this, new(session, data)); } catch (Exception ex) { @@ -230,7 +220,7 @@ private void RaiseSessionRemoved(Session session) { try { - OnSessionRemoved?.Invoke(this, new SquidStdSessionEventArgs(session)); + OnSessionRemoved?.Invoke(this, new(session)); } catch (Exception ex) { diff --git a/src/SquidStd.Network/Spans/SpanReader.cs b/src/SquidStd.Network/Spans/SpanReader.cs index ea982d9b..3168a69e 100644 --- a/src/SquidStd.Network/Spans/SpanReader.cs +++ b/src/SquidStd.Network/Spans/SpanReader.cs @@ -38,57 +38,39 @@ public int Read(scoped Span bytes) [MethodImpl(MethodImplOptions.AggressiveInlining)] public string ReadAscii(int fixedLength) - { - return ReadString(Encoding.ASCII, fixedLength: fixedLength); - } + => ReadString(Encoding.ASCII, fixedLength: fixedLength); [MethodImpl(MethodImplOptions.AggressiveInlining)] public string ReadAscii() - { - return ReadString(Encoding.ASCII); - } + => ReadString(Encoding.ASCII); [MethodImpl(MethodImplOptions.AggressiveInlining)] public string ReadAsciiSafe(int fixedLength) - { - return ReadString(Encoding.ASCII, true, fixedLength); - } + => ReadString(Encoding.ASCII, true, fixedLength); [MethodImpl(MethodImplOptions.AggressiveInlining)] public string ReadAsciiSafe() - { - return ReadString(Encoding.ASCII, true); - } + => ReadString(Encoding.ASCII, true); [MethodImpl(MethodImplOptions.AggressiveInlining)] public string ReadBigUni(int fixedLength) - { - return ReadString(Encoding.BigEndianUnicode, fixedLength: fixedLength); - } + => ReadString(Encoding.BigEndianUnicode, fixedLength: fixedLength); [MethodImpl(MethodImplOptions.AggressiveInlining)] public string ReadBigUni() - { - return ReadString(Encoding.BigEndianUnicode); - } + => ReadString(Encoding.BigEndianUnicode); [MethodImpl(MethodImplOptions.AggressiveInlining)] public string ReadBigUniSafe(int fixedLength) - { - return ReadString(Encoding.BigEndianUnicode, true, fixedLength); - } + => ReadString(Encoding.BigEndianUnicode, true, fixedLength); [MethodImpl(MethodImplOptions.AggressiveInlining)] public string ReadBigUniSafe() - { - return ReadString(Encoding.BigEndianUnicode, true); - } + => ReadString(Encoding.BigEndianUnicode, true); [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool ReadBoolean() - { - return ReadByte() > 0; - } + => ReadByte() > 0; [MethodImpl(MethodImplOptions.AggressiveInlining)] public byte ReadByte() @@ -195,33 +177,23 @@ public long ReadInt64LE() [MethodImpl(MethodImplOptions.AggressiveInlining)] public string ReadLittleUni(int fixedLength) - { - return ReadString(Encoding.Unicode, fixedLength: fixedLength); - } + => ReadString(Encoding.Unicode, fixedLength: fixedLength); [MethodImpl(MethodImplOptions.AggressiveInlining)] public string ReadLittleUni() - { - return ReadString(Encoding.Unicode); - } + => ReadString(Encoding.Unicode); [MethodImpl(MethodImplOptions.AggressiveInlining)] public string ReadLittleUniSafe(int fixedLength) - { - return ReadString(Encoding.Unicode, true, fixedLength); - } + => ReadString(Encoding.Unicode, true, fixedLength); [MethodImpl(MethodImplOptions.AggressiveInlining)] public string ReadLittleUniSafe() - { - return ReadString(Encoding.Unicode, true); - } + => ReadString(Encoding.Unicode, true); [MethodImpl(MethodImplOptions.AggressiveInlining)] public sbyte ReadSByte() - { - return (sbyte)ReadByte(); - } + => (sbyte)ReadByte(); [MethodImpl(MethodImplOptions.AggressiveInlining)] public string ReadString(Encoding encoding, bool safeString = false, int fixedLength = -1) @@ -345,21 +317,15 @@ public ulong ReadUInt64LE() [MethodImpl(MethodImplOptions.AggressiveInlining)] public string ReadUTF8() - { - return ReadString(Encoding.UTF8); - } + => ReadString(Encoding.UTF8); [MethodImpl(MethodImplOptions.AggressiveInlining)] public string ReadUTF8Safe(int fixedLength) - { - return ReadString(Encoding.UTF8, true, fixedLength); - } + => ReadString(Encoding.UTF8, true, fixedLength); [MethodImpl(MethodImplOptions.AggressiveInlining)] public string ReadUTF8Safe() - { - return ReadString(Encoding.UTF8, true); - } + => ReadString(Encoding.UTF8, true); [MethodImpl(MethodImplOptions.AggressiveInlining)] public int Seek(int offset, SeekOrigin origin) @@ -385,14 +351,12 @@ public int Seek(int offset, SeekOrigin origin) } private static int GetTerminatorWidth(Encoding encoding) - { - return encoding switch + => encoding switch { UnicodeEncoding => 2, UTF32Encoding => 4, _ => 1 }; - } private static int IndexOfTerminator(ReadOnlySpan span, int terminatorWidth) { @@ -426,9 +390,7 @@ private static int IndexOfTerminator(ReadOnlySpan span, int terminatorWidt [DoesNotReturn] private static void ThrowInsufficientData() - { - throw new InvalidOperationException("Insufficient data in buffer."); - } + => throw new InvalidOperationException("Insufficient data in buffer."); public void Dispose() { diff --git a/src/SquidStd.Network/Spans/SpanWriter.cs b/src/SquidStd.Network/Spans/SpanWriter.cs index f332a141..e7f2c053 100644 --- a/src/SquidStd.Network/Spans/SpanWriter.cs +++ b/src/SquidStd.Network/Spans/SpanWriter.cs @@ -75,9 +75,7 @@ public void EnsureCapacity(int capacity) } public ref byte GetPinnableReference() - { - return ref MemoryMarshal.GetReference(_buffer); - } + => ref MemoryMarshal.GetReference(_buffer); [MethodImpl(MethodImplOptions.NoInlining)] public void Grow(int additionalCapacity) @@ -151,7 +149,7 @@ public SpanOwner ToSpan() ArrayPool.Shared.Return(toReturn); } - return new SpanOwner(0, null, false); + return new(0, null, false); } // Capture the length BEFORE `this = default`, otherwise the reset zeroes @@ -163,14 +161,14 @@ public SpanOwner ToSpan() { this = default; - return new SpanOwner(length, currentPoolBuffer, true); + return new(length, currentPoolBuffer, true); } var ownedBuffer = ArrayPool.Shared.Rent(length); _buffer[..length].CopyTo(ownedBuffer); this = default; - return new SpanOwner(length, ownedBuffer, true); + return new(length, ownedBuffer, true); } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -286,21 +284,15 @@ public void Write(ReadOnlySpan value, Encoding encoding, int fixedLength = [MethodImpl(MethodImplOptions.AggressiveInlining)] public void WriteAscii(char chr) - { - Write((byte)chr); - } + => Write((byte)chr); [MethodImpl(MethodImplOptions.AggressiveInlining)] public void WriteAscii(string value) - { - Write(value.AsSpan(), Encoding.ASCII); - } + => Write(value.AsSpan(), Encoding.ASCII); [MethodImpl(MethodImplOptions.AggressiveInlining)] public void WriteAscii(string value, int fixedLength) - { - Write(value.AsSpan(), Encoding.ASCII, fixedLength); - } + => Write(value.AsSpan(), Encoding.ASCII, fixedLength); [MethodImpl(MethodImplOptions.AggressiveInlining)] public void WriteAsciiNull(string value) @@ -311,15 +303,11 @@ public void WriteAsciiNull(string value) [MethodImpl(MethodImplOptions.AggressiveInlining)] public void WriteBigUni(string value) - { - Write(value.AsSpan(), Encoding.BigEndianUnicode); - } + => Write(value.AsSpan(), Encoding.BigEndianUnicode); [MethodImpl(MethodImplOptions.AggressiveInlining)] public void WriteBigUni(string value, int fixedLength) - { - Write(value.AsSpan(), Encoding.BigEndianUnicode, fixedLength); - } + => Write(value.AsSpan(), Encoding.BigEndianUnicode, fixedLength); [MethodImpl(MethodImplOptions.AggressiveInlining)] public void WriteBigUniNull(string value) @@ -362,15 +350,11 @@ public void WriteLE(uint value) [MethodImpl(MethodImplOptions.AggressiveInlining)] public void WriteLittleUni(string value) - { - Write(value.AsSpan(), Encoding.Unicode); - } + => Write(value.AsSpan(), Encoding.Unicode); [MethodImpl(MethodImplOptions.AggressiveInlining)] public void WriteLittleUni(string value, int fixedLength) - { - Write(value.AsSpan(), Encoding.Unicode, fixedLength); - } + => Write(value.AsSpan(), Encoding.Unicode, fixedLength); [MethodImpl(MethodImplOptions.AggressiveInlining)] public void WriteLittleUniNull(string value) @@ -381,9 +365,7 @@ public void WriteLittleUniNull(string value) [MethodImpl(MethodImplOptions.AggressiveInlining)] public void WriteUTF8(string value) - { - Write(value.AsSpan(), Encoding.UTF8); - } + => Write(value.AsSpan(), Encoding.UTF8); [MethodImpl(MethodImplOptions.AggressiveInlining)] public void WriteUTF8Null(string value) @@ -393,14 +375,12 @@ public void WriteUTF8Null(string value) } private static int GetTerminatorWidth(Encoding encoding) - { - return encoding switch + => encoding switch { UnicodeEncoding => 2, UTF32Encoding => 4, _ => 1 }; - } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void GrowIfNeeded(int count) @@ -431,7 +411,7 @@ public void Dispose() } /// - /// Represents SpanOwner. + /// Represents SpanOwner. /// public struct SpanOwner : IDisposable { diff --git a/src/SquidStd.Persistence.Abstractions/Interfaces/Persistence/ISnapshotService.cs b/src/SquidStd.Persistence.Abstractions/Interfaces/Persistence/ISnapshotService.cs index fc772362..987217c3 100644 --- a/src/SquidStd.Persistence.Abstractions/Interfaces/Persistence/ISnapshotService.cs +++ b/src/SquidStd.Persistence.Abstractions/Interfaces/Persistence/ISnapshotService.cs @@ -8,10 +8,14 @@ public interface ISnapshotService ValueTask DeleteBucketAsync(string typeName, ushort typeId, CancellationToken cancellationToken = default); ValueTask LoadBucketAsync( - string typeName, ushort typeId, CancellationToken cancellationToken = default + string typeName, + ushort typeId, + CancellationToken cancellationToken = default ); ValueTask SaveBucketAsync( - EntitySnapshotBucket bucket, long lastSequenceId, CancellationToken cancellationToken = default + EntitySnapshotBucket bucket, + long lastSequenceId, + CancellationToken cancellationToken = default ); } diff --git a/src/SquidStd.Persistence.Abstractions/README.md b/src/SquidStd.Persistence.Abstractions/README.md index 1276938d..9cef6e0f 100644 --- a/src/SquidStd.Persistence.Abstractions/README.md +++ b/src/SquidStd.Persistence.Abstractions/README.md @@ -11,17 +11,17 @@ dotnet add package SquidStd.Persistence.Abstractions ## Key types -| Type | Purpose | -|---------------------------------------|---------------------------------------------------------------| -| `IPersistenceService` | Lifecycle (`ISquidStdService`) + `GetStore()`. | -| `IEntityStore` | In-memory CRUD contract (clones on read, journals on write). | -| `IPersistenceEntityRegistry` | Registry of entity descriptors keyed by `typeId`/type pair. | -| `IPersistenceEntityDescriptor`| Serialize / deserialize / clone / key extraction contract. | -| `ISnapshotService` / `IJournalService`| Per-type snapshot and append-only journal contracts. | -| `PersistenceConfig` | Autosave cadence, file names, save directory, file lock. | -| `JournalEntry`, `EntitySnapshotBucket`, `PersistedBucket`, `SnapshotFileEnvelope` | Persisted DTOs. | -| `JournalEntityOperationType` | `Upsert` / `Remove` journal operation discriminator. | -| `SnapshotSaveStartedEvent` / `SnapshotSaveCompletedEvent` | Snapshot lifecycle events (`IEvent`). | +| Type | Purpose | +|-----------------------------------------------------------------------------------|--------------------------------------------------------------| +| `IPersistenceService` | Lifecycle (`ISquidStdService`) + `GetStore()`. | +| `IEntityStore` | In-memory CRUD contract (clones on read, journals on write). | +| `IPersistenceEntityRegistry` | Registry of entity descriptors keyed by `typeId`/type pair. | +| `IPersistenceEntityDescriptor` | Serialize / deserialize / clone / key extraction contract. | +| `ISnapshotService` / `IJournalService` | Per-type snapshot and append-only journal contracts. | +| `PersistenceConfig` | Autosave cadence, file names, save directory, file lock. | +| `JournalEntry`, `EntitySnapshotBucket`, `PersistedBucket`, `SnapshotFileEnvelope` | Persisted DTOs. | +| `JournalEntityOperationType` | `Upsert` / `Remove` journal operation discriminator. | +| `SnapshotSaveStartedEvent` / `SnapshotSaveCompletedEvent` | Snapshot lifecycle events (`IEvent`). | ## License diff --git a/src/SquidStd.Persistence.MessagePack/README.md b/src/SquidStd.Persistence.MessagePack/README.md index 643f25f6..16eff37d 100644 --- a/src/SquidStd.Persistence.MessagePack/README.md +++ b/src/SquidStd.Persistence.MessagePack/README.md @@ -29,9 +29,9 @@ container.RegisterInstance(new MessagePackDataSerializer()); ## Key types -| Type | Purpose | -|----------------------------|----------------------------------------------------------| -| `MessagePackDataSerializer`| Contractless MessagePack `IDataSerializer`/`IDataDeserializer`. | +| Type | Purpose | +|-----------------------------|-----------------------------------------------------------------| +| `MessagePackDataSerializer` | Contractless MessagePack `IDataSerializer`/`IDataDeserializer`. | ## License diff --git a/src/SquidStd.Persistence/Data/PersistenceEntityDescriptor.cs b/src/SquidStd.Persistence/Data/PersistenceEntityDescriptor.cs index 1170c6cf..9397b539 100644 --- a/src/SquidStd.Persistence/Data/PersistenceEntityDescriptor.cs +++ b/src/SquidStd.Persistence/Data/PersistenceEntityDescriptor.cs @@ -86,7 +86,7 @@ void IInternalEntityApplier.ApplyRemove(PersistenceStateStore stateStore, byte[] return null; } - return new EntitySnapshotBucket + return new() { TypeId = TypeId, TypeName = TypeName, diff --git a/src/SquidStd.Persistence/Internal/JournalRecordCodec.cs b/src/SquidStd.Persistence/Internal/JournalRecordCodec.cs index 43686e84..90cfc951 100644 --- a/src/SquidStd.Persistence/Internal/JournalRecordCodec.cs +++ b/src/SquidStd.Persistence/Internal/JournalRecordCodec.cs @@ -36,7 +36,7 @@ public static JournalEntry Decode(ReadOnlySpan bytes) var payloadLength = BinaryPrimitives.ReadInt32LittleEndian(bytes[19..]); var payload = bytes.Slice(FixedHeader, payloadLength).ToArray(); - return new JournalEntry + return new() { SequenceId = sequenceId, TimestampUnixMilliseconds = timestamp, diff --git a/src/SquidStd.Persistence/Internal/SnapshotEnvelopeCodec.cs b/src/SquidStd.Persistence/Internal/SnapshotEnvelopeCodec.cs index b0931f92..a2dd7dca 100644 --- a/src/SquidStd.Persistence/Internal/SnapshotEnvelopeCodec.cs +++ b/src/SquidStd.Persistence/Internal/SnapshotEnvelopeCodec.cs @@ -72,12 +72,12 @@ public static SnapshotFileEnvelope Decode(ReadOnlySpan bytes) offset += 4; var payload = bytes.Slice(offset, payloadLength).ToArray(); - return new SnapshotFileEnvelope + return new() { Version = version, LastSequenceId = lastSequenceId, Checksum = checksum, - Bucket = new EntitySnapshotBucket + Bucket = new() { TypeId = typeId, TypeName = typeName, diff --git a/src/SquidStd.Persistence/README.md b/src/SquidStd.Persistence/README.md index 39dde4a9..6700d011 100644 --- a/src/SquidStd.Persistence/README.md +++ b/src/SquidStd.Persistence/README.md @@ -56,15 +56,15 @@ container.ApplyPersistedEntityRegistrations(); // builds descriptors into IPer ## Key types -| Type | Purpose | -|---------------------------------------|---------------------------------------------------------------| -| `PersistenceService` | Lifecycle: load + replay, autosave, `GetStore()`. | -| `IEntityStore` | In-memory CRUD; reads clone, writes journal. | -| `PersistenceEntityDescriptor` | Serializer-injected descriptor (serialize/clone/key). | -| `PersistenceEntityRegistry` | Maps `typeId` ↔ descriptor; freezes after registration. | -| `BinaryJournalService` | Append-only framed binary WAL with tail-corruption recovery. | -| `SnapshotService` | Atomic per-type binary snapshot files with payload checksum. | -| `RegisterPersistedEntity()` | DI helper recording an entity for descriptor construction. | +| Type | Purpose | +|---------------------------------------|--------------------------------------------------------------| +| `PersistenceService` | Lifecycle: load + replay, autosave, `GetStore()`. | +| `IEntityStore` | In-memory CRUD; reads clone, writes journal. | +| `PersistenceEntityDescriptor` | Serializer-injected descriptor (serialize/clone/key). | +| `PersistenceEntityRegistry` | Maps `typeId` ↔ descriptor; freezes after registration. | +| `BinaryJournalService` | Append-only framed binary WAL with tail-corruption recovery. | +| `SnapshotService` | Atomic per-type binary snapshot files with payload checksum. | +| `RegisterPersistedEntity()` | DI helper recording an entity for descriptor construction. | ### Durability diff --git a/src/SquidStd.Persistence/Services/BinaryJournalService.cs b/src/SquidStd.Persistence/Services/BinaryJournalService.cs index 308dfbec..9a334b2c 100644 --- a/src/SquidStd.Persistence/Services/BinaryJournalService.cs +++ b/src/SquidStd.Persistence/Services/BinaryJournalService.cs @@ -55,7 +55,7 @@ public async ValueTask AppendAsync(JournalEntry entry, CancellationToken cancell if (_durability == DurabilityMode.Durable) { - stream.Flush(flushToDisk: true); + stream.Flush(true); } } finally @@ -65,7 +65,8 @@ public async ValueTask AppendAsync(JournalEntry entry, CancellationToken cancell } public async ValueTask AppendBatchAsync( - IReadOnlyList entries, CancellationToken cancellationToken = default + IReadOnlyList entries, + CancellationToken cancellationToken = default ) { ArgumentNullException.ThrowIfNull(entries); @@ -83,7 +84,7 @@ public async ValueTask AppendBatchAsync( if (_durability == DurabilityMode.Durable) { - stream.Flush(flushToDisk: true); + stream.Flush(true); } } finally @@ -137,8 +138,8 @@ public async ValueTask TrimThroughSequenceAsync(long inclusiveSequenceId, Cancel } var kept = ParseAll(await File.ReadAllBytesAsync(_path, cancellationToken)) - .Where(entry => entry.SequenceId > inclusiveSequenceId) - .ToArray(); + .Where(entry => entry.SequenceId > inclusiveSequenceId) + .ToArray(); await RewriteAsync(kept, cancellationToken); } @@ -152,7 +153,7 @@ private FileStream OpenAppendStream() { var options = _durability == DurabilityMode.Durable ? FileOptions.WriteThrough : FileOptions.None; - return new FileStream(_path, FileMode.Append, FileAccess.Write, FileShare.Read, bufferSize: 4096, options); + return new(_path, FileMode.Append, FileAccess.Write, FileShare.Read, 4096, options); } private List ParseAll(byte[] bytes) @@ -208,7 +209,9 @@ private async ValueTask RewriteAsync(IReadOnlyList entries, Cancel } private static async ValueTask WriteRecordAsync( - FileStream stream, JournalEntry entry, CancellationToken cancellationToken + FileStream stream, + JournalEntry entry, + CancellationToken cancellationToken ) { var record = JournalRecordCodec.Encode(entry); diff --git a/src/SquidStd.Persistence/Services/EntityStore.cs b/src/SquidStd.Persistence/Services/EntityStore.cs index e2f51e18..95de6fa8 100644 --- a/src/SquidStd.Persistence/Services/EntityStore.cs +++ b/src/SquidStd.Persistence/Services/EntityStore.cs @@ -78,7 +78,7 @@ public async ValueTask UpsertAsync(TEntity entity, CancellationToken cancellatio clone = _descriptor.Clone(entity); key = _descriptor.GetKey(clone); next = _stateStore.LastSequenceId + 1; // computed, not yet committed - entry = new JournalEntry + entry = new() { SequenceId = next, TimestampUnixMilliseconds = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(), @@ -120,7 +120,7 @@ public async ValueTask RemoveAsync(TKey id, CancellationToken cancellation } next = _stateStore.LastSequenceId + 1; - entry = new JournalEntry + entry = new() { SequenceId = next, TimestampUnixMilliseconds = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(), diff --git a/src/SquidStd.Persistence/Services/PersistenceService.cs b/src/SquidStd.Persistence/Services/PersistenceService.cs index 17d4e2c9..33d6dd2f 100644 --- a/src/SquidStd.Persistence/Services/PersistenceService.cs +++ b/src/SquidStd.Persistence/Services/PersistenceService.cs @@ -234,7 +234,5 @@ private async Task AutosaveLoopAsync(CancellationToken cancellationToken) } public async ValueTask DisposeAsync() - { - await StopAsync(CancellationToken.None); - } + => await StopAsync(CancellationToken.None); } diff --git a/src/SquidStd.Persistence/Services/SnapshotService.cs b/src/SquidStd.Persistence/Services/SnapshotService.cs index 2212bcb5..9edaeb41 100644 --- a/src/SquidStd.Persistence/Services/SnapshotService.cs +++ b/src/SquidStd.Persistence/Services/SnapshotService.cs @@ -60,7 +60,9 @@ public async ValueTask DeleteBucketAsync(string typeName, ushort typeId, Cancell } public async ValueTask LoadBucketAsync( - string typeName, ushort typeId, CancellationToken cancellationToken = default + string typeName, + ushort typeId, + CancellationToken cancellationToken = default ) { ArgumentException.ThrowIfNullOrWhiteSpace(typeName); @@ -91,8 +93,8 @@ public async ValueTask DeleteBucketAsync(string typeName, ushort typeId, Cancell var envelope = SnapshotEnvelopeCodec.Decode(bytes); var checksumValid = envelope.Version >= 2 - ? SnapshotEnvelopeCodec.ComputeFullChecksum(bytes) == envelope.Checksum - : ChecksumUtils.Compute(envelope.Bucket.Payload) == envelope.Checksum; + ? SnapshotEnvelopeCodec.ComputeFullChecksum(bytes) == envelope.Checksum + : ChecksumUtils.Compute(envelope.Bucket.Payload) == envelope.Checksum; if (!checksumValid || !string.Equals(envelope.Bucket.TypeName, typeName, StringComparison.Ordinal)) { @@ -101,7 +103,7 @@ public async ValueTask DeleteBucketAsync(string typeName, ushort typeId, Cancell return null; } - return new PersistedBucket(envelope.Bucket, envelope.LastSequenceId); + return new(envelope.Bucket, envelope.LastSequenceId); } catch (OperationCanceledException) { @@ -120,7 +122,9 @@ public async ValueTask DeleteBucketAsync(string typeName, ushort typeId, Cancell } public async ValueTask SaveBucketAsync( - EntitySnapshotBucket bucket, long lastSequenceId, CancellationToken cancellationToken = default + EntitySnapshotBucket bucket, + long lastSequenceId, + CancellationToken cancellationToken = default ) { ArgumentNullException.ThrowIfNull(bucket); @@ -153,7 +157,7 @@ public async ValueTask SaveBucketAsync( if (_durability == DurabilityMode.Durable) { - stream.Flush(flushToDisk: true); // fsync the temp file before the atomic rename + stream.Flush(true); // fsync the temp file before the atomic rename } else { @@ -170,14 +174,10 @@ public async ValueTask SaveBucketAsync( } private string PathFor(string typeName, ushort typeId) - { - return Path.Combine(_directory, SnakeName(typeName) + "_" + typeId + _suffix); - } + => Path.Combine(_directory, SnakeName(typeName) + "_" + typeId + _suffix); private string LegacyPathFor(string typeName) - { - return Path.Combine(_directory, SnakeName(typeName) + _suffix); - } + => Path.Combine(_directory, SnakeName(typeName) + _suffix); private string SnakeName(string typeName) { diff --git a/src/SquidStd.Plugin.Abstractions/Data/PluginContext.cs b/src/SquidStd.Plugin.Abstractions/Data/PluginContext.cs index 1d511a0a..93fed50e 100644 --- a/src/SquidStd.Plugin.Abstractions/Data/PluginContext.cs +++ b/src/SquidStd.Plugin.Abstractions/Data/PluginContext.cs @@ -5,7 +5,5 @@ public class PluginContext public Dictionary Data { get; } = new(); public TData GetData(string key) - { - return (TData)Data[key]; - } + => (TData)Data[key]; } diff --git a/src/SquidStd.Plugin.Abstractions/Data/PluginMetadata.cs b/src/SquidStd.Plugin.Abstractions/Data/PluginMetadata.cs index 65672cfa..eeffa2f3 100644 --- a/src/SquidStd.Plugin.Abstractions/Data/PluginMetadata.cs +++ b/src/SquidStd.Plugin.Abstractions/Data/PluginMetadata.cs @@ -1,7 +1,7 @@ namespace SquidStd.Plugin.Abstractions.Data; /// -/// Describes a Moongate plugin. This is the source of truth for plugin identity. +/// Describes a Moongate plugin. This is the source of truth for plugin identity. /// public sealed class PluginMetadata { diff --git a/src/SquidStd.Plugin.Abstractions/Interfaces/Plugins/ISquidStdPlugin.cs b/src/SquidStd.Plugin.Abstractions/Interfaces/Plugins/ISquidStdPlugin.cs index 7b575827..d761f5a0 100644 --- a/src/SquidStd.Plugin.Abstractions/Interfaces/Plugins/ISquidStdPlugin.cs +++ b/src/SquidStd.Plugin.Abstractions/Interfaces/Plugins/ISquidStdPlugin.cs @@ -4,7 +4,7 @@ namespace SquidStd.Plugin.Abstractions.Interfaces.Plugins; /// -/// Implemented by trusted .NET plugins loaded by Moongate during server startup. +/// Implemented by trusted .NET plugins loaded by Moongate during server startup. /// public interface ISquidStdPlugin { @@ -12,8 +12,8 @@ public interface ISquidStdPlugin PluginMetadata Metadata { get; } /// - /// Registers the plugin's services, handlers, config sections, Lua modules, and other integrations. - /// Called during container configuration before global server YAML config is loaded. + /// Registers the plugin's services, handlers, config sections, Lua modules, and other integrations. + /// Called during container configuration before global server YAML config is loaded. /// /// The DryIoc container being configured. /// The plugin-specific boot context. diff --git a/src/SquidStd.Scripting.Lua/Attributes/RegisterScriptModuleAttribute.cs b/src/SquidStd.Scripting.Lua/Attributes/RegisterScriptModuleAttribute.cs index c714e3b2..df3f076f 100644 --- a/src/SquidStd.Scripting.Lua/Attributes/RegisterScriptModuleAttribute.cs +++ b/src/SquidStd.Scripting.Lua/Attributes/RegisterScriptModuleAttribute.cs @@ -1,9 +1,7 @@ namespace SquidStd.Scripting.Lua.Attributes; /// -/// Marks a Lua script module for generated registration. +/// Marks a Lua script module for generated registration. /// [AttributeUsage(AttributeTargets.Class, Inherited = false)] -public sealed class RegisterScriptModuleAttribute : Attribute -{ -} +public sealed class RegisterScriptModuleAttribute : Attribute { } diff --git a/src/SquidStd.Scripting.Lua/Attributes/Scripts/ScriptFunctionAttribute.cs b/src/SquidStd.Scripting.Lua/Attributes/Scripts/ScriptFunctionAttribute.cs index a724f8ab..5f20e670 100644 --- a/src/SquidStd.Scripting.Lua/Attributes/Scripts/ScriptFunctionAttribute.cs +++ b/src/SquidStd.Scripting.Lua/Attributes/Scripts/ScriptFunctionAttribute.cs @@ -1,23 +1,23 @@ namespace SquidStd.Scripting.Lua.Attributes.Scripts; /// -/// Attribute that marks a method as a script function exposed to scripting languages. +/// Attribute that marks a method as a script function exposed to scripting languages. /// [AttributeUsage(AttributeTargets.Method)] public class ScriptFunctionAttribute : Attribute { /// - /// Gets the optional name override for the script function. + /// Gets the optional name override for the script function. /// public string? FunctionName { get; } /// - /// Gets the optional help text describing the function's purpose. + /// Gets the optional help text describing the function's purpose. /// public string? HelpText { get; } /// - /// Initializes a new instance of the ScriptFunctionAttribute class. + /// Initializes a new instance of the ScriptFunctionAttribute class. /// /// The optional name override for the script function. /// The optional help text describing the function's purpose. diff --git a/src/SquidStd.Scripting.Lua/Attributes/Scripts/ScriptModuleAttribute.cs b/src/SquidStd.Scripting.Lua/Attributes/Scripts/ScriptModuleAttribute.cs index b12b47c2..0e54b821 100644 --- a/src/SquidStd.Scripting.Lua/Attributes/Scripts/ScriptModuleAttribute.cs +++ b/src/SquidStd.Scripting.Lua/Attributes/Scripts/ScriptModuleAttribute.cs @@ -1,7 +1,7 @@ namespace SquidStd.Scripting.Lua.Attributes.Scripts; /// -/// Attribute that marks a class as a script module exposed to scripting languages. +/// Attribute that marks a class as a script module exposed to scripting languages. /// [AttributeUsage(AttributeTargets.Class)] public class ScriptModuleAttribute : Attribute @@ -13,7 +13,7 @@ public class ScriptModuleAttribute : Attribute public string? HelpText { get; } /// - /// Initializes a new instance of the ScriptModuleAttribute class. + /// Initializes a new instance of the ScriptModuleAttribute class. /// /// The name under which the module will be accessible in Lua. /// The optional help text describing the module's purpose. diff --git a/src/SquidStd.Scripting.Lua/Context/SquidStdScriptJsonContext.cs b/src/SquidStd.Scripting.Lua/Context/SquidStdScriptJsonContext.cs index 485076cb..d55ec080 100644 --- a/src/SquidStd.Scripting.Lua/Context/SquidStdScriptJsonContext.cs +++ b/src/SquidStd.Scripting.Lua/Context/SquidStdScriptJsonContext.cs @@ -3,15 +3,11 @@ namespace SquidStd.Scripting.Lua.Context; -[JsonSerializable(typeof(LuarcConfig))] -[JsonSerializable(typeof(LuarcRuntimeConfig))] -[JsonSerializable(typeof(LuarcWorkspaceConfig))] -[JsonSerializable(typeof(LuarcDiagnosticsConfig))] -[JsonSerializable(typeof(LuarcCompletionConfig))] -[JsonSerializable(typeof(LuarcFormatConfig))] +[JsonSerializable(typeof(LuarcConfig)), JsonSerializable(typeof(LuarcRuntimeConfig)), + JsonSerializable(typeof(LuarcWorkspaceConfig)), JsonSerializable(typeof(LuarcDiagnosticsConfig)), + JsonSerializable(typeof(LuarcCompletionConfig)), JsonSerializable(typeof(LuarcFormatConfig))] + /// /// JSON serialization context for Lua scripting configuration types. /// -public partial class SquidStdScriptJsonContext : JsonSerializerContext -{ -} +public partial class SquidStdScriptJsonContext : JsonSerializerContext { } diff --git a/src/SquidStd.Scripting.Lua/Data/Internal/ScriptModuleData.cs b/src/SquidStd.Scripting.Lua/Data/Internal/ScriptModuleData.cs index bde2dd25..531d682d 100644 --- a/src/SquidStd.Scripting.Lua/Data/Internal/ScriptModuleData.cs +++ b/src/SquidStd.Scripting.Lua/Data/Internal/ScriptModuleData.cs @@ -1,17 +1,17 @@ namespace SquidStd.Scripting.Lua.Data.Internal; /// -/// Record containing data about a script module for internal processing. +/// Record containing data about a script module for internal processing. /// public sealed record ScriptModuleData { /// - /// The .NET type of the script module. + /// The .NET type of the script module. /// public Type ModuleType { get; } /// - /// Initializes a new instance of the ScriptModuleData record. + /// Initializes a new instance of the ScriptModuleData record. /// /// The .NET type of the script module. public ScriptModuleData(Type moduleType) diff --git a/src/SquidStd.Scripting.Lua/Data/Internal/ScriptUserData.cs b/src/SquidStd.Scripting.Lua/Data/Internal/ScriptUserData.cs index a002f05f..6f518bbb 100644 --- a/src/SquidStd.Scripting.Lua/Data/Internal/ScriptUserData.cs +++ b/src/SquidStd.Scripting.Lua/Data/Internal/ScriptUserData.cs @@ -1,12 +1,12 @@ namespace SquidStd.Scripting.Lua.Data.Internal; /// -/// Represents user data for scripts. +/// Represents user data for scripts. /// public class ScriptUserData { /// - /// Gets or sets the user type. + /// Gets or sets the user type. /// public Type UserType { get; set; } } diff --git a/src/SquidStd.Scripting.Lua/Data/Luarc/LuarcCompletionConfig.cs b/src/SquidStd.Scripting.Lua/Data/Luarc/LuarcCompletionConfig.cs index 112af62e..b7086634 100644 --- a/src/SquidStd.Scripting.Lua/Data/Luarc/LuarcCompletionConfig.cs +++ b/src/SquidStd.Scripting.Lua/Data/Luarc/LuarcCompletionConfig.cs @@ -3,18 +3,18 @@ namespace SquidStd.Scripting.Lua.Data.Luarc; /// -/// Completion configuration for Lua Language Server +/// Completion configuration for Lua Language Server /// public class LuarcCompletionConfig { /// - /// Gets or sets whether completion is enabled. + /// Gets or sets whether completion is enabled. /// [JsonPropertyName("enable")] public bool Enable { get; set; } = true; /// - /// Gets or sets the call snippet setting. + /// Gets or sets the call snippet setting. /// [JsonPropertyName("callSnippet")] public string CallSnippet { get; set; } = "Replace"; diff --git a/src/SquidStd.Scripting.Lua/Data/Luarc/LuarcConfig.cs b/src/SquidStd.Scripting.Lua/Data/Luarc/LuarcConfig.cs index bf5b2e58..8726bd11 100644 --- a/src/SquidStd.Scripting.Lua/Data/Luarc/LuarcConfig.cs +++ b/src/SquidStd.Scripting.Lua/Data/Luarc/LuarcConfig.cs @@ -3,7 +3,7 @@ namespace SquidStd.Scripting.Lua.Data.Luarc; /// -/// Configuration class for Lua Language Server (.luarc.json file) +/// Configuration class for Lua Language Server (.luarc.json file) /// public class LuarcConfig { @@ -15,31 +15,31 @@ public class LuarcConfig public string Schema { get; set; } = "https://raw.githubusercontent.com/sumneko/vscode-lua/master/setting/schema.json"; /// - /// Gets or sets the runtime configuration. + /// Gets or sets the runtime configuration. /// [JsonPropertyName("runtime")] public LuarcRuntimeConfig Runtime { get; set; } = new(); /// - /// Gets or sets the workspace configuration. + /// Gets or sets the workspace configuration. /// [JsonPropertyName("workspace")] public LuarcWorkspaceConfig Workspace { get; set; } = new(); /// - /// Gets or sets the diagnostics configuration. + /// Gets or sets the diagnostics configuration. /// [JsonPropertyName("diagnostics")] public LuarcDiagnosticsConfig Diagnostics { get; set; } = new(); /// - /// Gets or sets the completion configuration. + /// Gets or sets the completion configuration. /// [JsonPropertyName("completion")] public LuarcCompletionConfig Completion { get; set; } = new(); /// - /// Gets or sets the format configuration. + /// Gets or sets the format configuration. /// [JsonPropertyName("format")] public LuarcFormatConfig Format { get; set; } = new(); diff --git a/src/SquidStd.Scripting.Lua/Data/Luarc/LuarcDiagnosticsConfig.cs b/src/SquidStd.Scripting.Lua/Data/Luarc/LuarcDiagnosticsConfig.cs index 777a2b3d..bb22e469 100644 --- a/src/SquidStd.Scripting.Lua/Data/Luarc/LuarcDiagnosticsConfig.cs +++ b/src/SquidStd.Scripting.Lua/Data/Luarc/LuarcDiagnosticsConfig.cs @@ -3,9 +3,10 @@ namespace SquidStd.Scripting.Lua.Data.Luarc; /// -/// Diagnostics configuration for Lua Language Server +/// Diagnostics configuration for Lua Language Server /// public class LuarcDiagnosticsConfig { - [JsonPropertyName("globals")] public string[] Globals { get; set; } = []; + [JsonPropertyName("globals")] + public string[] Globals { get; set; } = []; } diff --git a/src/SquidStd.Scripting.Lua/Data/Luarc/LuarcFormatConfig.cs b/src/SquidStd.Scripting.Lua/Data/Luarc/LuarcFormatConfig.cs index 5b823013..0828099e 100644 --- a/src/SquidStd.Scripting.Lua/Data/Luarc/LuarcFormatConfig.cs +++ b/src/SquidStd.Scripting.Lua/Data/Luarc/LuarcFormatConfig.cs @@ -3,11 +3,13 @@ namespace SquidStd.Scripting.Lua.Data.Luarc; /// -/// Format configuration for Lua Language Server +/// Format configuration for Lua Language Server /// public class LuarcFormatConfig { - [JsonPropertyName("enable")] public bool Enable { get; set; } = true; + [JsonPropertyName("enable")] + public bool Enable { get; set; } = true; - [JsonPropertyName("defaultConfig")] public LuarcFormatDefaultConfig DefaultConfig { get; set; } = new(); + [JsonPropertyName("defaultConfig")] + public LuarcFormatDefaultConfig DefaultConfig { get; set; } = new(); } diff --git a/src/SquidStd.Scripting.Lua/Data/Luarc/LuarcFormatDefaultConfig.cs b/src/SquidStd.Scripting.Lua/Data/Luarc/LuarcFormatDefaultConfig.cs index 0456d94e..e34ca695 100644 --- a/src/SquidStd.Scripting.Lua/Data/Luarc/LuarcFormatDefaultConfig.cs +++ b/src/SquidStd.Scripting.Lua/Data/Luarc/LuarcFormatDefaultConfig.cs @@ -3,11 +3,13 @@ namespace SquidStd.Scripting.Lua.Data.Luarc; /// -/// Default format configuration for Lua Language Server +/// Default format configuration for Lua Language Server /// public class LuarcFormatDefaultConfig { - [JsonPropertyName("indent_style")] public string IndentStyle { get; set; } = "space"; + [JsonPropertyName("indent_style")] + public string IndentStyle { get; set; } = "space"; - [JsonPropertyName("indent_size")] public string IndentSize { get; set; } = "4"; + [JsonPropertyName("indent_size")] + public string IndentSize { get; set; } = "4"; } diff --git a/src/SquidStd.Scripting.Lua/Data/Luarc/LuarcRuntimeConfig.cs b/src/SquidStd.Scripting.Lua/Data/Luarc/LuarcRuntimeConfig.cs index 0ac03aa8..b6ecbe4c 100644 --- a/src/SquidStd.Scripting.Lua/Data/Luarc/LuarcRuntimeConfig.cs +++ b/src/SquidStd.Scripting.Lua/Data/Luarc/LuarcRuntimeConfig.cs @@ -3,11 +3,13 @@ namespace SquidStd.Scripting.Lua.Data.Luarc; /// -/// Runtime configuration for Lua Language Server +/// Runtime configuration for Lua Language Server /// public class LuarcRuntimeConfig { - [JsonPropertyName("version")] public string Version { get; set; } = "Lua 5.4"; + [JsonPropertyName("version")] + public string Version { get; set; } = "Lua 5.4"; - [JsonPropertyName("path")] public string[] Path { get; set; } = []; + [JsonPropertyName("path")] + public string[] Path { get; set; } = []; } diff --git a/src/SquidStd.Scripting.Lua/Data/Luarc/LuarcWorkspaceConfig.cs b/src/SquidStd.Scripting.Lua/Data/Luarc/LuarcWorkspaceConfig.cs index fa114261..72ba1948 100644 --- a/src/SquidStd.Scripting.Lua/Data/Luarc/LuarcWorkspaceConfig.cs +++ b/src/SquidStd.Scripting.Lua/Data/Luarc/LuarcWorkspaceConfig.cs @@ -3,11 +3,13 @@ namespace SquidStd.Scripting.Lua.Data.Luarc; /// -/// Workspace configuration for Lua Language Server +/// Workspace configuration for Lua Language Server /// public class LuarcWorkspaceConfig { - [JsonPropertyName("library")] public string[] Library { get; set; } = []; + [JsonPropertyName("library")] + public string[] Library { get; set; } = []; - [JsonPropertyName("checkThirdParty")] public bool CheckThirdParty { get; set; } = false; + [JsonPropertyName("checkThirdParty")] + public bool CheckThirdParty { get; set; } = false; } diff --git a/src/SquidStd.Scripting.Lua/Data/Scripts/ScriptErrorInfo.cs b/src/SquidStd.Scripting.Lua/Data/Scripts/ScriptErrorInfo.cs index 41d257af..5b88190f 100644 --- a/src/SquidStd.Scripting.Lua/Data/Scripts/ScriptErrorInfo.cs +++ b/src/SquidStd.Scripting.Lua/Data/Scripts/ScriptErrorInfo.cs @@ -1,62 +1,62 @@ namespace SquidStd.Scripting.Lua.Data.Scripts; /// -/// Detailed information about a Lua execution error. +/// Detailed information about a Lua execution error. /// public class ScriptErrorInfo { /// - /// Gets or sets the error message. + /// Gets or sets the error message. /// public string Message { get; set; } = ""; /// - /// Gets or sets the stack trace. + /// Gets or sets the stack trace. /// public string? StackTrace { get; set; } /// - /// Gets or sets the line number. + /// Gets or sets the line number. /// public int? LineNumber { get; set; } /// - /// Gets or sets the column number. + /// Gets or sets the column number. /// public int? ColumnNumber { get; set; } /// - /// Gets or sets the file name. + /// Gets or sets the file name. /// public string? FileName { get; set; } /// - /// Gets or sets the error type. + /// Gets or sets the error type. /// public string? ErrorType { get; set; } /// - /// Gets or sets the source code. + /// Gets or sets the source code. /// public string? SourceCode { get; set; } /// - /// Original source file name when a mapped source is available. + /// Original source file name when a mapped source is available. /// public string? OriginalFileName { get; set; } /// - /// Original line number when a mapped source is available. + /// Original line number when a mapped source is available. /// public int? OriginalLineNumber { get; set; } /// - /// Original column number when a mapped source is available. + /// Original column number when a mapped source is available. /// public int? OriginalColumnNumber { get; set; } /// - /// Optional origin label — e.g. the Lua component name when the error came from a lifecycle hook. + /// Optional origin label — e.g. the Lua component name when the error came from a lifecycle hook. /// public string? Source { get; set; } } diff --git a/src/SquidStd.Scripting.Lua/Data/Scripts/ScriptExecutionMetrics.cs b/src/SquidStd.Scripting.Lua/Data/Scripts/ScriptExecutionMetrics.cs index aa2dd0f2..a14d7fd1 100644 --- a/src/SquidStd.Scripting.Lua/Data/Scripts/ScriptExecutionMetrics.cs +++ b/src/SquidStd.Scripting.Lua/Data/Scripts/ScriptExecutionMetrics.cs @@ -1,37 +1,37 @@ namespace SquidStd.Scripting.Lua.Data.Scripts; /// -/// Metrics about script execution performance. +/// Metrics about script execution performance. /// public class ScriptExecutionMetrics { /// - /// Gets or sets the execution time in milliseconds. + /// Gets or sets the execution time in milliseconds. /// public long ExecutionTimeMs { get; set; } /// - /// Gets or sets the memory used in bytes. + /// Gets or sets the memory used in bytes. /// public long MemoryUsedBytes { get; set; } /// - /// Gets or sets the number of statements executed. + /// Gets or sets the number of statements executed. /// public int StatementsExecuted { get; set; } /// - /// Gets or sets the number of cache hits. + /// Gets or sets the number of cache hits. /// public int CacheHits { get; set; } /// - /// Gets or sets the number of cache misses. + /// Gets or sets the number of cache misses. /// public int CacheMisses { get; set; } /// - /// Gets or sets the total number of scripts cached. + /// Gets or sets the total number of scripts cached. /// public int TotalScriptsCached { get; set; } } diff --git a/src/SquidStd.Scripting.Lua/Data/Scripts/ScriptResult.cs b/src/SquidStd.Scripting.Lua/Data/Scripts/ScriptResult.cs index 79f37619..a6e991f1 100644 --- a/src/SquidStd.Scripting.Lua/Data/Scripts/ScriptResult.cs +++ b/src/SquidStd.Scripting.Lua/Data/Scripts/ScriptResult.cs @@ -1,22 +1,22 @@ namespace SquidStd.Scripting.Lua.Data.Scripts; /// -/// Represents the result of a script execution. +/// Represents the result of a script execution. /// public class ScriptResult { /// - /// Gets or sets a value indicating whether the script execution was successful. + /// Gets or sets a value indicating whether the script execution was successful. /// public bool Success { get; set; } /// - /// Gets or sets the message associated with the script result. + /// Gets or sets the message associated with the script result. /// public string Message { get; set; } /// - /// Gets or sets the data returned by the script execution. + /// Gets or sets the data returned by the script execution. /// public object? Data { get; set; } } diff --git a/src/SquidStd.Scripting.Lua/Data/Scripts/ScriptResultBuilder.cs b/src/SquidStd.Scripting.Lua/Data/Scripts/ScriptResultBuilder.cs index 0c13c2d5..147af4e1 100644 --- a/src/SquidStd.Scripting.Lua/Data/Scripts/ScriptResultBuilder.cs +++ b/src/SquidStd.Scripting.Lua/Data/Scripts/ScriptResultBuilder.cs @@ -1,7 +1,7 @@ namespace SquidStd.Scripting.Lua.Data.Scripts; /// -/// Builder class for creating ScriptResult instances. +/// Builder class for creating ScriptResult instances. /// public class ScriptResultBuilder { @@ -10,36 +10,30 @@ public class ScriptResultBuilder private bool _success; /// - /// Builds the ScriptResult instance. + /// Builds the ScriptResult instance. /// public ScriptResult Build() - { - return new ScriptResult + => new() { Success = _success, Message = _message, Data = _data }; - } /// - /// Creates a ScriptResultBuilder initialized for an error result. + /// Creates a ScriptResultBuilder initialized for an error result. /// public static ScriptResultBuilder CreateError() - { - return new ScriptResultBuilder().WithSuccess(false); - } + => new ScriptResultBuilder().WithSuccess(false); /// - /// Creates a ScriptResultBuilder initialized for a successful result. + /// Creates a ScriptResultBuilder initialized for a successful result. /// public static ScriptResultBuilder CreateSuccess() - { - return new ScriptResultBuilder().WithSuccess(true); - } + => new ScriptResultBuilder().WithSuccess(true); /// - /// Sets the result as failed. + /// Sets the result as failed. /// public ScriptResultBuilder Failure() { @@ -49,7 +43,7 @@ public ScriptResultBuilder Failure() } /// - /// Sets the result as successful. + /// Sets the result as successful. /// public ScriptResultBuilder Success() { @@ -59,7 +53,7 @@ public ScriptResultBuilder Success() } /// - /// Sets the data of the result. + /// Sets the data of the result. /// public ScriptResultBuilder WithData(object? data) { @@ -69,7 +63,7 @@ public ScriptResultBuilder WithData(object? data) } /// - /// Sets the message of the result. + /// Sets the message of the result. /// public ScriptResultBuilder WithMessage(string message) { @@ -79,7 +73,7 @@ public ScriptResultBuilder WithMessage(string message) } /// - /// Sets the success status of the result. + /// Sets the success status of the result. /// public ScriptResultBuilder WithSuccess(bool success) { diff --git a/src/SquidStd.Scripting.Lua/Descriptors/GenericUserDataDescriptor.cs b/src/SquidStd.Scripting.Lua/Descriptors/GenericUserDataDescriptor.cs index 458658b6..245c9db8 100644 --- a/src/SquidStd.Scripting.Lua/Descriptors/GenericUserDataDescriptor.cs +++ b/src/SquidStd.Scripting.Lua/Descriptors/GenericUserDataDescriptor.cs @@ -4,16 +4,16 @@ namespace SquidStd.Scripting.Lua.Descriptors; /// -/// Generic UserData descriptor that adds support for string concatenation and conversion. -/// Implements the __tostring metamethod to allow Lua to convert any userdata type to strings. +/// Generic UserData descriptor that adds support for string concatenation and conversion. +/// Implements the __tostring metamethod to allow Lua to convert any userdata type to strings. /// public class GenericUserDataDescriptor : StandardUserDataDescriptor { private readonly bool _isXnaType; /// - /// Creates a new descriptor for a type with reflection access mode. - /// Automatically detects if the type is an XNA Framework type. + /// Creates a new descriptor for a type with reflection access mode. + /// Automatically detects if the type is an XNA Framework type. /// /// The type to describe (can be XNA or any other .NET type). public GenericUserDataDescriptor(Type type) @@ -24,17 +24,17 @@ public GenericUserDataDescriptor(Type type) } /// - /// Converts the object to its string representation. - /// This is used by MoonSharp when the object is converted to a string in Lua (concatenation, tostring(), etc.). + /// Converts the object to its string representation. + /// This is used by MoonSharp when the object is converted to a string in Lua (concatenation, tostring(), etc.). /// /// The object to convert to string. /// - /// For XNA types: Uses ToString() for a readable representation. - /// For other types: Uses ToString() or the type name if ToString() returns null. + /// For XNA types: Uses ToString() for a readable representation. + /// For other types: Uses ToString() or the type name if ToString() returns null. /// /// - /// In Lua, this allows: - /// + /// In Lua, this allows: + /// /// local vec = Vector2(10, 20) /// print("Position: " .. vec) -- ✅ Calls AsString(vec) instead of erroring /// local str = tostring(vec) -- ✅ Works with tostring() @@ -51,10 +51,10 @@ public override string AsString(object obj) var str = obj.ToString(); return string.IsNullOrWhiteSpace(str) - ? + ? - // Fallback: use the type name if ToString() returns empty/null - $"{Type.Name}({{}})" - : str; + // Fallback: use the type name if ToString() returns empty/null + $"{Type.Name}({{}})" + : str; } } diff --git a/src/SquidStd.Scripting.Lua/Extensions/Scripts/AddScriptModuleExtension.cs b/src/SquidStd.Scripting.Lua/Extensions/Scripts/AddScriptModuleExtension.cs index df5c2fb8..4cf0e5f0 100644 --- a/src/SquidStd.Scripting.Lua/Extensions/Scripts/AddScriptModuleExtension.cs +++ b/src/SquidStd.Scripting.Lua/Extensions/Scripts/AddScriptModuleExtension.cs @@ -6,7 +6,7 @@ namespace SquidStd.Scripting.Lua.Extensions.Scripts; /// -/// Extension methods for registering Lua script modules in the dependency injection container. +/// Extension methods for registering Lua script modules in the dependency injection container. /// public static class AddScriptModuleExtension { @@ -14,7 +14,7 @@ public static class AddScriptModuleExtension extension(IContainer container) { /// - /// Registers a user data type with the container for Lua scripting. + /// Registers a user data type with the container for Lua scripting. /// public IContainer RegisterLuaUserData(Type userDataType) { @@ -29,7 +29,7 @@ public IContainer RegisterLuaUserData(Type userDataType) } /// - /// Registers a user data type with the container for Lua scripting using generics. + /// Registers a user data type with the container for Lua scripting using generics. /// public IContainer RegisterLuaUserData() { @@ -39,7 +39,7 @@ public IContainer RegisterLuaUserData() } /// - /// Registers a Lua script module type with the container. + /// Registers a Lua script module type with the container. /// /// The type of the script module to register. /// The container for method chaining. @@ -59,13 +59,11 @@ public IContainer RegisterScriptModule(Type scriptModule) } /// - /// Registers a Lua script module type with the container using a generic type parameter. + /// Registers a Lua script module type with the container using a generic type parameter. /// /// The type of the script module to register. /// The container for method chaining. public IContainer RegisterScriptModule() where TScriptModule : class - { - return container.RegisterScriptModule(typeof(TScriptModule)); - } + => container.RegisterScriptModule(typeof(TScriptModule)); } } diff --git a/src/SquidStd.Scripting.Lua/Extensions/Scripts/TableExtensions.cs b/src/SquidStd.Scripting.Lua/Extensions/Scripts/TableExtensions.cs index 945ea825..76fe5311 100644 --- a/src/SquidStd.Scripting.Lua/Extensions/Scripts/TableExtensions.cs +++ b/src/SquidStd.Scripting.Lua/Extensions/Scripts/TableExtensions.cs @@ -5,14 +5,14 @@ namespace SquidStd.Scripting.Lua.Extensions.Scripts; /// -/// Provides extension methods for MoonSharp Table objects to enable proxying to interfaces. +/// Provides extension methods for MoonSharp Table objects to enable proxying to interfaces. /// public static class TableExtensions { extension(Table table) { /// - /// Converts a MoonSharp Table to a proxy implementing the specified interface. + /// Converts a MoonSharp Table to a proxy implementing the specified interface. /// public TInterface ToProxy() where TInterface : class diff --git a/src/SquidStd.Scripting.Lua/Interfaces/Events/ILuaEventBridge.cs b/src/SquidStd.Scripting.Lua/Interfaces/Events/ILuaEventBridge.cs index fd74fdca..d7e37dbb 100644 --- a/src/SquidStd.Scripting.Lua/Interfaces/Events/ILuaEventBridge.cs +++ b/src/SquidStd.Scripting.Lua/Interfaces/Events/ILuaEventBridge.cs @@ -3,18 +3,18 @@ namespace SquidStd.Scripting.Lua.Interfaces.Events; /// -/// Bridges named server events and internal callbacks into Lua closures. +/// Bridges named server events and internal callbacks into Lua closures. /// public interface ILuaEventBridge { /// - /// Attaches the active MoonSharp script runtime used to invoke registered closures. + /// Attaches the active MoonSharp script runtime used to invoke registered closures. /// /// Active Lua script runtime. void Attach(Script script); /// - /// Invokes a single closure with the supplied payload. + /// Invokes a single closure with the supplied payload. /// /// Lua closure to invoke. /// Payload exposed to Lua as a table. @@ -22,14 +22,14 @@ public interface ILuaEventBridge DynValue Invoke(Closure callback, IReadOnlyDictionary payload); /// - /// Publishes a named event to every Lua callback registered for it. + /// Publishes a named event to every Lua callback registered for it. /// /// Stable event name, such as player.connected. /// Payload exposed to Lua as a table. void Publish(string eventName, IReadOnlyDictionary payload); /// - /// Registers a Lua callback for a named event. + /// Registers a Lua callback for a named event. /// /// Stable event name, such as player.connected. /// Lua closure to invoke when the event is published. diff --git a/src/SquidStd.Scripting.Lua/Interfaces/Scripts/IScriptEngineService.cs b/src/SquidStd.Scripting.Lua/Interfaces/Scripts/IScriptEngineService.cs index 5d4783b6..7cc7403a 100644 --- a/src/SquidStd.Scripting.Lua/Interfaces/Scripts/IScriptEngineService.cs +++ b/src/SquidStd.Scripting.Lua/Interfaces/Scripts/IScriptEngineService.cs @@ -4,32 +4,32 @@ namespace SquidStd.Scripting.Lua.Interfaces.Scripts; /// -/// Interface for the script engine service that manages Lua execution. +/// Interface for the script engine service that manages Lua execution. /// public interface IScriptEngineService : ISquidStdService { /// - /// Adds a callback function that can be called from Lua. + /// Adds a callback function that can be called from Lua. /// /// The name of the callback function in Lua. /// The C# action to execute when the callback is invoked. void AddCallback(string name, Action callback); /// - /// Adds a constant value accessible from Lua. + /// Adds a constant value accessible from Lua. /// /// The name of the constant in Lua. /// The value of the constant. void AddConstant(string name, object value); /// - /// Adds a script to be executed during engine initialization. + /// Adds a script to be executed during engine initialization. /// /// The Lua code to execute on startup. void AddInitScript(string script); /// - /// Adds a manual module function that can be called from scripts. + /// Adds a manual module function that can be called from scripts. /// /// The name of the module. /// The name of the function. @@ -37,7 +37,7 @@ public interface IScriptEngineService : ISquidStdService void AddManualModuleFunction(string moduleName, string functionName, Action callback); /// - /// Adds a typed manual module function that can be called from scripts. + /// Adds a typed manual module function that can be called from scripts. /// /// The input parameter type. /// The output return type. @@ -51,43 +51,43 @@ void AddManualModuleFunction( ); /// - /// Adds a .NET type as a module accessible from Lua. + /// Adds a .NET type as a module accessible from Lua. /// /// The type to register as a script module. void AddScriptModule(Type type); /// - /// Adds a directory to the Lua script search paths. + /// Adds a directory to the Lua script search paths. /// /// Directory path to search for scripts. void AddSearchDirectory(string path); /// - /// Clears the script cache + /// Clears the script cache /// void ClearScriptCache(); /// - /// Executes a previously registered callback function. + /// Executes a previously registered callback function. /// /// The name of the callback to execute. /// Arguments to pass to the callback. void ExecuteCallback(string name, params object[] args); /// - /// Notifies the script engine that the engine initialization is complete and ready. + /// Notifies the script engine that the engine initialization is complete and ready. /// void ExecuteEngineReady(); /// - /// Executes a Lua function or expression and returns the result. + /// Executes a Lua function or expression and returns the result. /// /// The Lua function call or expression to execute. /// A ScriptResult containing the execution outcome. ScriptResult ExecuteFunction(string command); /// - /// Asynchronously executes a Lua function or expression and returns the result. + /// Asynchronously executes a Lua function or expression and returns the result. /// /// The Lua function call or expression to execute. /// Token used to cancel the operation. @@ -95,87 +95,87 @@ void AddManualModuleFunction( Task ExecuteFunctionAsync(string command, CancellationToken cancellationToken = default); /// - /// Executes a function defined in the bootstrap script. + /// Executes a function defined in the bootstrap script. /// /// void ExecuteFunctionFromBootstrap(string name); /// - /// Executes a Lua script string. + /// Executes a Lua script string. /// /// The Lua code to execute. void ExecuteScript(string script); /// - /// Executes a Lua file. + /// Executes a Lua file. /// /// The path to the Lua file to execute. void ExecuteScriptFile(string scriptFile); /// - /// Gets execution metrics for performance monitoring + /// Gets execution metrics for performance monitoring /// /// Metrics about script execution ScriptExecutionMetrics GetExecutionMetrics(); /// - /// Registers a global object/value accessible from scripts. + /// Registers a global object/value accessible from scripts. /// /// The name of the global in scripts. /// The object/value to register. void RegisterGlobal(string name, object value); /// - /// Registers a global function that can be called from scripts. + /// Registers a global function that can be called from scripts. /// /// The name of the global function in scripts. /// The delegate to register as a global function. void RegisterGlobalFunction(string name, Delegate func); /// - /// Converts a .NET method name to a Lua-compatible function name. + /// Converts a .NET method name to a Lua-compatible function name. /// /// The .NET method name to convert. /// The Lua-compatible function name. string ToScriptEngineFunctionName(string name); /// - /// Unregisters a global function or value. + /// Unregisters a global function or value. /// /// The name of the global to unregister. /// True if the global was found and removed, false otherwise. bool UnregisterGlobal(string name); /// - /// Delegate for handling script file change events. + /// Delegate for handling script file change events. /// /// The path to the changed file. /// True if the file change was handled successfully, false otherwise. delegate bool LuaFileChangedHandler(string filePath); /// - /// Event raised when a script file is modified. + /// Event raised when a script file is modified. /// event LuaFileChangedHandler? FileChanged; /// - /// Event raised when a script error occurs + /// Event raised when a script error occurs /// event EventHandler? OnScriptError; /// - /// Fires once during StartAsync, after script modules have been registered - /// but before bootstrap scripts run. Handlers can install additional UserData types, globals, - /// or scanners that depend on the script runtime being ready. The argument is the underlying - /// MoonSharp Script, typed as so the interface stays - /// implementation-agnostic; callers cast as needed. + /// Fires once during StartAsync, after script modules have been registered + /// but before bootstrap scripts run. Handlers can install additional UserData types, globals, + /// or scanners that depend on the script runtime being ready. The argument is the underlying + /// MoonSharp Script, typed as so the interface stays + /// implementation-agnostic; callers cast as needed. /// event Action? AfterModulesRegistered; /// - /// Fires when a .lua file under a components/ subdirectory of the scripts - /// directory changes on disk. Carries the full file path. Used by the engine-side - /// component loader to hot-reload Lua-defined components. + /// Fires when a .lua file under a components/ subdirectory of the scripts + /// directory changes on disk. Carries the full file path. Used by the engine-side + /// component loader to hot-reload Lua-defined components. /// event Action? OnComponentFileChanged; } diff --git a/src/SquidStd.Scripting.Lua/Loaders/LuaScriptLoader.cs b/src/SquidStd.Scripting.Lua/Loaders/LuaScriptLoader.cs index 0ee14b7b..f4571e88 100644 --- a/src/SquidStd.Scripting.Lua/Loaders/LuaScriptLoader.cs +++ b/src/SquidStd.Scripting.Lua/Loaders/LuaScriptLoader.cs @@ -6,8 +6,8 @@ namespace SquidStd.Scripting.Lua.Loaders; /// -/// Custom script loader for MoonSharp that loads Lua modules from the configured Scripts directory. -/// Implements the MoonSharp script loader interface to provide require() functionality. +/// Custom script loader for MoonSharp that loads Lua modules from the configured Scripts directory. +/// Implements the MoonSharp script loader interface to provide require() functionality. /// public class LuaScriptLoader : ScriptLoaderBase { @@ -15,14 +15,14 @@ public class LuaScriptLoader : ScriptLoaderBase private readonly List _scriptsDirectories; /// - /// Initializes a new instance of the LuaScriptLoader class. + /// Initializes a new instance of the LuaScriptLoader class. /// /// The directories configuration to resolve the scripts directory. public LuaScriptLoader(string rootDirectory) { ArgumentNullException.ThrowIfNull(rootDirectory); - _scriptsDirectories = new List { Path.GetFullPath(rootDirectory) }; + _scriptsDirectories = new() { Path.GetFullPath(rootDirectory) }; // Configure default module search paths ModulePaths = @@ -37,13 +37,11 @@ public LuaScriptLoader(string rootDirectory) } /// - /// Initializes a new instance of the LuaScriptLoader class with multiple search directories. + /// Initializes a new instance of the LuaScriptLoader class with multiple search directories. /// /// Ordered list of directories to search. public LuaScriptLoader(IReadOnlyList searchDirectories) - : this(searchDirectories, true) - { - } + : this(searchDirectories, true) { } private LuaScriptLoader(IReadOnlyList searchDirectories, bool _) { @@ -55,9 +53,9 @@ private LuaScriptLoader(IReadOnlyList searchDirectories, bool _) } _scriptsDirectories = searchDirectories.Where(d => !string.IsNullOrWhiteSpace(d)) - .Select(d => Path.GetFullPath(d.ResolvePathAndEnvs()).ResolvePathAndEnvs()) - .Distinct(StringComparer.OrdinalIgnoreCase) - .ToList(); + .Select(d => Path.GetFullPath(d.ResolvePathAndEnvs()).ResolvePathAndEnvs()) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToList(); if (_scriptsDirectories.Count == 0) { @@ -94,7 +92,7 @@ public void AddSearchDirectory(string directory) } /// - /// Loads a Lua script file from the configured scripts directory. + /// Loads a Lua script file from the configured scripts directory. /// /// The filename or module name to load. /// The global context table. @@ -132,7 +130,7 @@ public override object LoadFile(string file, Table globalContext) } /// - /// Checks if a script file exists in the configured scripts directory. + /// Checks if a script file exists in the configured scripts directory. /// /// The filename or module name to check. /// True if the file exists, false otherwise. @@ -147,7 +145,7 @@ public override bool ScriptFileExists(string name) } /// - /// Resolves a module name to a full file path by searching through configured module paths. + /// Resolves a module name to a full file path by searching through configured module paths. /// /// The module name to resolve. /// The full path to the module file, or null if not found. diff --git a/src/SquidStd.Scripting.Lua/Modules/EventsModule.cs b/src/SquidStd.Scripting.Lua/Modules/EventsModule.cs index 366f1cc6..9137a3e8 100644 --- a/src/SquidStd.Scripting.Lua/Modules/EventsModule.cs +++ b/src/SquidStd.Scripting.Lua/Modules/EventsModule.cs @@ -16,7 +16,5 @@ public EventsModule(ILuaEventBridge events) [ScriptFunction("on", "Registers a callback for a named server event.")] public void On(string eventName, Closure callback) - { - _events.Register(eventName, callback); - } + => _events.Register(eventName, callback); } diff --git a/src/SquidStd.Scripting.Lua/Modules/LogModule.cs b/src/SquidStd.Scripting.Lua/Modules/LogModule.cs index c28979d7..dd462dbc 100644 --- a/src/SquidStd.Scripting.Lua/Modules/LogModule.cs +++ b/src/SquidStd.Scripting.Lua/Modules/LogModule.cs @@ -10,19 +10,13 @@ public class LogModule [ScriptFunction(helpText: "Logs a message at the ERROR level.")] public void Error(string message, params object[]? args) - { - _logger.Error(message, args); - } + => _logger.Error(message, args); [ScriptFunction(helpText: "Logs a message at the INFO level.")] public void Info(string message, params object[]? args) - { - _logger.Information(message, args); - } + => _logger.Information(message, args); [ScriptFunction(helpText: "Logs a message at the WARNING level.")] public void Warning(string message, params object[]? args) - { - _logger.Warning(message, args); - } + => _logger.Warning(message, args); } diff --git a/src/SquidStd.Scripting.Lua/Modules/RandomModule.cs b/src/SquidStd.Scripting.Lua/Modules/RandomModule.cs index a60d7e67..4547ab25 100644 --- a/src/SquidStd.Scripting.Lua/Modules/RandomModule.cs +++ b/src/SquidStd.Scripting.Lua/Modules/RandomModule.cs @@ -24,9 +24,7 @@ public bool Chance(double percent) [ScriptFunction("float", "Returns a random floating-point number between 0 and 1.")] public double Float() - { - return Random.Shared.NextDouble(); - } + => Random.Shared.NextDouble(); [ScriptFunction("int", "Returns a random integer in the inclusive range.")] public int Int(int min, int max) diff --git a/src/SquidStd.Scripting.Lua/Proxies/LuaProxy.cs b/src/SquidStd.Scripting.Lua/Proxies/LuaProxy.cs index 36d8cae5..bf5c8bd1 100644 --- a/src/SquidStd.Scripting.Lua/Proxies/LuaProxy.cs +++ b/src/SquidStd.Scripting.Lua/Proxies/LuaProxy.cs @@ -4,7 +4,7 @@ namespace SquidStd.Scripting.Lua.Proxies; /// -/// A proxy class that implements an interface by delegating method calls to a MoonSharp Table. +/// A proxy class that implements an interface by delegating method calls to a MoonSharp Table. /// /// The interface type to implement. public class LuaProxy : DispatchProxy @@ -12,7 +12,7 @@ public class LuaProxy : DispatchProxy public Table Table { get; set; } /// - /// Invokes the Lua function corresponding to the method name on the associated table. + /// Invokes the Lua function corresponding to the method name on the associated table. /// /// The method information for the invoked method. /// The arguments passed to the method. @@ -27,8 +27,8 @@ protected override object Invoke(MethodInfo targetMethod, object[] args) } var dynArgs = args - .Select(a => DynValue.FromObject(null, a)) - .ToArray(); + .Select(a => DynValue.FromObject(null, a)) + .ToArray(); var result = fn.Function.Call(dynArgs); return result.ToObject(targetMethod.ReturnType); diff --git a/src/SquidStd.Scripting.Lua/Services/LuaEventBridge.cs b/src/SquidStd.Scripting.Lua/Services/LuaEventBridge.cs index 8201739e..fe473fb3 100644 --- a/src/SquidStd.Scripting.Lua/Services/LuaEventBridge.cs +++ b/src/SquidStd.Scripting.Lua/Services/LuaEventBridge.cs @@ -6,7 +6,7 @@ namespace SquidStd.Scripting.Lua.Services; /// -/// Default Lua event bridge backed by named MoonSharp closures. +/// Default Lua event bridge backed by named MoonSharp closures. /// public sealed class LuaEventBridge : ILuaEventBridge { diff --git a/src/SquidStd.Scripting.Lua/Services/LuaScriptEngineService.cs b/src/SquidStd.Scripting.Lua/Services/LuaScriptEngineService.cs index cbd3c2db..b075355b 100644 --- a/src/SquidStd.Scripting.Lua/Services/LuaScriptEngineService.cs +++ b/src/SquidStd.Scripting.Lua/Services/LuaScriptEngineService.cs @@ -30,8 +30,8 @@ namespace SquidStd.Scripting.Lua.Services; /// -/// Lua engine service that integrates MoonSharp with the SquidCraft game engine -/// Provides script execution, module loading, and Lua meta file generation +/// Lua engine service that integrates MoonSharp with the SquidCraft game engine +/// Provides script execution, module loading, and Lua meta file generation /// public class LuaScriptEngineService : IScriptEngineService, IDisposable { @@ -62,17 +62,17 @@ public class LuaScriptEngineService : IScriptEngineService, IDisposable private FileSystemWatcher? _watcher; /// - /// Gets the MoonSharp script instance. + /// Gets the MoonSharp script instance. /// public Script LuaScript { get; } /// - /// Gets the script engine instance. + /// Gets the script engine instance. /// public object Engine => LuaScript; /// - /// Initializes a new instance of the LuaScriptEngineService class. + /// Initializes a new instance of the LuaScriptEngineService class. /// /// The directories configuration. /// The list of script modules. @@ -89,8 +89,8 @@ public LuaScriptEngineService( { JsonUtils.RegisterJsonContext(SquidStdScriptJsonContext.Default); - scriptModules ??= new List(); - loadedUserData ??= new List(); + scriptModules ??= new(); + loadedUserData ??= new(); _scriptModules = scriptModules; _directoriesConfig = directoriesConfig; @@ -107,7 +107,7 @@ public LuaScriptEngineService( } /// - /// Adds a callback function that can be called from Lua scripts. + /// Adds a callback function that can be called from Lua scripts. /// /// The name of the callback. /// The callback action. @@ -123,7 +123,7 @@ public void AddCallback(string name, Action callback) } /// - /// Adds a constant value that can be accessed from Lua scripts. + /// Adds a constant value that can be accessed from Lua scripts. /// /// The name of the constant. /// The value of the constant. @@ -153,7 +153,7 @@ public void AddConstant(string name, object? value) } /// - /// Adds an initialization script. + /// Adds an initialization script. /// /// The script to add. public void AddInitScript(string script) @@ -167,7 +167,7 @@ public void AddInitScript(string script) } /// - /// Adds a manual module function that can be called from Lua scripts with a callback. + /// Adds a manual module function that can be called from Lua scripts with a callback. /// public void AddManualModuleFunction(string moduleName, string functionName, Action callback) { @@ -177,7 +177,8 @@ public void AddManualModuleFunction(string moduleName, string functionName, Acti var (normalizedModule, normalizedFunction, moduleTable) = PrepareManualModule(moduleName, functionName); - moduleTable[normalizedFunction] = DynValue.NewCallback((_, args) => + moduleTable[normalizedFunction] = DynValue.NewCallback( + (_, args) => { try { @@ -204,7 +205,7 @@ public void AddManualModuleFunction(string moduleName, string functionName, Acti } /// - /// Adds a manual module function with typed input and output that can be called from Lua scripts. + /// Adds a manual module function with typed input and output that can be called from Lua scripts. /// public void AddManualModuleFunction( string moduleName, @@ -218,7 +219,8 @@ public void AddManualModuleFunction( var (normalizedModule, normalizedFunction, moduleTable) = PrepareManualModule(moduleName, functionName); - moduleTable[normalizedFunction] = DynValue.NewCallback((_, args) => + moduleTable[normalizedFunction] = DynValue.NewCallback( + (_, args) => { try { @@ -245,22 +247,20 @@ public void AddManualModuleFunction( } /// - /// Adds a script module to the engine. + /// Adds a script module to the engine. /// /// The type of the script module. public void AddScriptModule(Type type) { ArgumentNullException.ThrowIfNull(type); - _scriptModules.Add(new ScriptModuleData(type)); + _scriptModules.Add(new(type)); } public void AddSearchDirectory(string path) - { - _scriptLoader.AddSearchDirectory(path); - } + => _scriptLoader.AddSearchDirectory(path); /// - /// Clears the script cache + /// Clears the script cache /// public void ClearScriptCache() { @@ -271,7 +271,7 @@ public void ClearScriptCache() } /// - /// Executes a registered callback with the specified arguments. + /// Executes a registered callback with the specified arguments. /// /// The name of the callback. /// The arguments to pass to the callback. @@ -302,15 +302,13 @@ public void ExecuteCallback(string name, params object[] args) } /// - /// Executes the engine ready function from bootstrap scripts. + /// Executes the engine ready function from bootstrap scripts. /// public void ExecuteEngineReady() - { - ExecuteFunctionFromBootstrap(OnEngineRunFunctionName); - } + => ExecuteFunctionFromBootstrap(OnEngineRunFunctionName); /// - /// Executes a Lua function and returns the result. + /// Executes a Lua function and returns the result. /// /// The function command to execute. /// The result of the function execution. @@ -336,10 +334,10 @@ public ScriptResult ExecuteFunction(string command) ); return ScriptResultBuilder.CreateError() - .WithMessage( - $"{errorInfo.ErrorType}: {errorInfo.Message} at line {errorInfo.LineNumber}" - ) - .Build(); + .WithMessage( + $"{errorInfo.ErrorType}: {errorInfo.Message} at line {errorInfo.LineNumber}" + ) + .Build(); } catch (InterpreterException luaEx) { @@ -356,10 +354,10 @@ public ScriptResult ExecuteFunction(string command) ); return ScriptResultBuilder.CreateError() - .WithMessage( - $"{errorInfo.ErrorType}: {errorInfo.Message} at line {errorInfo.LineNumber}" - ) - .Build(); + .WithMessage( + $"{errorInfo.ErrorType}: {errorInfo.Message} at line {errorInfo.LineNumber}" + ) + .Build(); } catch (Exception ex) { @@ -370,7 +368,7 @@ public ScriptResult ExecuteFunction(string command) } /// - /// Executes a Lua function asynchronously and returns the result. + /// Executes a Lua function asynchronously and returns the result. /// /// The function command to execute. /// Token used to cancel the operation. @@ -419,16 +417,14 @@ public void ExecuteFunctionFromBootstrap(string name) } /// - /// Executes a script string. + /// Executes a script string. /// /// The script to execute. public void ExecuteScript(string script) - { - ExecuteScript(script, null); - } + => ExecuteScript(script, null); /// - /// Executes a script from a file. + /// Executes a script from a file. /// /// The path to the script file. public void ExecuteScriptFile(string scriptFile) @@ -453,20 +449,18 @@ public void ExecuteScriptFile(string scriptFile) } /// - /// Gets execution metrics for performance monitoring + /// Gets execution metrics for performance monitoring /// public ScriptExecutionMetrics GetExecutionMetrics() - { - return new ScriptExecutionMetrics + => new() { CacheHits = _cacheHits, CacheMisses = _cacheMisses, TotalScriptsCached = _scriptCache.Count }; - } /// - /// Registers a global variable in the Lua environment. + /// Registers a global variable in the Lua environment. /// /// The name of the variable. /// The value of the variable. @@ -480,7 +474,7 @@ public void RegisterGlobal(string name, object value) } /// - /// Registers a global function in the Lua environment. + /// Registers a global function in the Lua environment. /// /// The name of the function. /// The delegate representing the function. @@ -494,7 +488,7 @@ public void RegisterGlobalFunction(string name, Delegate func) } /// - /// Starts the script engine asynchronously. + /// Starts the script engine asynchronously. /// /// A task representing the asynchronous operation. public async ValueTask StartAsync(CancellationToken cancellationToken = default) @@ -531,7 +525,7 @@ public async ValueTask StartAsync(CancellationToken cancellationToken = default) if (_watcher == null) { - _watcher = new FileSystemWatcher(_engineConfig.ScriptsDirectory, "*.lua") + _watcher = new(_engineConfig.ScriptsDirectory, "*.lua") { NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName | NotifyFilters.Size, IncludeSubdirectories = true, @@ -550,16 +544,14 @@ public async ValueTask StartAsync(CancellationToken cancellationToken = default) } /// - /// Stops the script engine asynchronously. + /// Stops the script engine asynchronously. /// /// A task representing the asynchronous operation. public ValueTask StopAsync(CancellationToken cancellationToken = default) - { - return ValueTask.CompletedTask; - } + => ValueTask.CompletedTask; /// - /// Converts a name to the script engine function name format. + /// Converts a name to the script engine function name format. /// /// The name to convert. /// The converted function name. @@ -571,7 +563,7 @@ public string ToScriptEngineFunctionName(string name) } /// - /// Unregisters a global variable from the Lua environment. + /// Unregisters a global variable from the Lua environment. /// /// The name of the variable to unregister. /// True if the variable was unregistered, false otherwise. @@ -595,7 +587,7 @@ public bool UnregisterGlobal(string name) } /// - /// Executes a script file asynchronously. + /// Executes a script file asynchronously. /// /// The path to the script file. /// Token used to cancel the operation. @@ -624,16 +616,14 @@ public async Task ExecuteScriptFileAsync(string scriptFile, CancellationToken ca } /// - /// Gets the statistics of the script engine. + /// Gets the statistics of the script engine. /// /// A tuple containing the module count, callback count, constant count, and initialization status. public (int ModuleCount, int CallbackCount, int ConstantCount, bool IsInitialized) GetStats() - { - return (_loadedModules.Count, _callbacks.Count, _constants.Count, _isInitialized); - } + => (_loadedModules.Count, _callbacks.Count, _constants.Count, _isInitialized); /// - /// Registers a global type user data. + /// Registers a global type user data. /// /// The type to register. public void RegisterGlobalTypeUserData(Type type) @@ -646,7 +636,7 @@ public void RegisterGlobalTypeUserData(Type type) } /// - /// Registers a global type user data for the specified type. + /// Registers a global type user data for the specified type. /// /// The type to register. public void RegisterGlobalTypeUserData() @@ -658,7 +648,7 @@ public void RegisterGlobalTypeUserData() } /// - /// Resets the script engine to its initial state. + /// Resets the script engine to its initial state. /// public void Reset() { @@ -696,8 +686,7 @@ private void AttachLuaEventBridge() } private static object? ConvertFromLua(DynValue dynValue, Type targetType) - { - return dynValue.Type switch + => dynValue.Type switch { DataType.Nil => null, DataType.Boolean => dynValue.Boolean, @@ -706,19 +695,15 @@ private void AttachLuaEventBridge() DataType.Table => dynValue.ToObject(), _ => dynValue.ToObject() }; - } private DynValue ConvertToLua(object? value) - { - return value == null ? DynValue.Nil : DynValue.FromObject(LuaScript, value); - } + => value == null ? DynValue.Nil : DynValue.FromObject(LuaScript, value); /// - /// Creates a Lua callback that invokes the constructor matching the number of arguments passed from Lua. + /// Creates a Lua callback that invokes the constructor matching the number of arguments passed from Lua. /// private DynValue CreateConstructorCallback( - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] - Type type + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type type ) { var constructorsByParamCount = new Dictionary(); @@ -730,7 +715,8 @@ Type type constructorsByParamCount.TryAdd(paramCount, ctor); } - return DynValue.NewCallback((_, args) => + return DynValue.NewCallback( + (_, args) => { var argCount = args.Count; @@ -767,7 +753,7 @@ Type type } /// - /// Creates detailed error information from a Lua exception + /// Creates detailed error information from a Lua exception /// private static ScriptErrorInfo CreateErrorInfo(ScriptRuntimeException luaEx, string sourceCode, string? fileName = null) { @@ -786,7 +772,7 @@ private static ScriptErrorInfo CreateErrorInfo(ScriptRuntimeException luaEx, str } /// - /// Creates detailed error information from a Lua interpreter exception (syntax errors, etc.) + /// Creates detailed error information from a Lua interpreter exception (syntax errors, etc.) /// private static ScriptErrorInfo CreateErrorInfo(InterpreterException luaEx, string sourceCode, string? fileName = null) { @@ -830,8 +816,8 @@ private static ScriptErrorInfo CreateErrorInfo(InterpreterException luaEx, strin } private DynValue CreateMethodClosure(object instance, MethodInfo method) - { - return DynValue.NewCallback((context, args) => + => DynValue.NewCallback( + (context, args) => { try { @@ -890,7 +876,6 @@ private DynValue CreateMethodClosure(object instance, MethodInfo method) } } ); - } private Table CreateModuleTable( object instance, @@ -901,7 +886,7 @@ Type moduleType var moduleTable = new Table(LuaScript); var methods = moduleType.GetMethods(BindingFlags.Public | BindingFlags.Instance) - .Where(m => m.GetCustomAttribute() is not null); + .Where(m => m.GetCustomAttribute() is not null); foreach (var method in methods) { @@ -913,8 +898,8 @@ Type moduleType } var functionName = string.IsNullOrWhiteSpace(scriptFunctionAttr.FunctionName) - ? _nameResolver(method.Name) - : scriptFunctionAttr.FunctionName; + ? _nameResolver(method.Name) + : scriptFunctionAttr.FunctionName; // Create a closure that captures the instance and method var closure = CreateMethodClosure(instance, method); @@ -925,9 +910,7 @@ Type moduleType } private void CreateNameResolver() - { - _nameResolver = name => name.ToSnakeCase(); - } + => _nameResolver = name => name.ToSnakeCase(); // _nameResolver = _scriptEngineConfig.ScriptNameConversion switch // { @@ -938,7 +921,7 @@ private void CreateNameResolver() // }; private Script CreateOptimizedEngine() { - _scriptLoader = new LuaScriptLoader(new[] { _engineConfig.ScriptsDirectory }); + _scriptLoader = new(new[] { _engineConfig.ScriptsDirectory }); var script = new Script { Options = @@ -955,9 +938,7 @@ private Script CreateOptimizedEngine() } private void ExecuteBootFunction() - { - ExecuteFunctionFromBootstrap(OnReadyFunctionName); - } + => ExecuteFunctionFromBootstrap(OnReadyFunctionName); private void ExecuteBootstrap() { @@ -973,7 +954,7 @@ private void ExecuteBootstrap() } /// - /// Executes a script string with an optional file name for error reporting. + /// Executes a script string with an optional file name for error reporting. /// /// The script to execute. /// Optional file name for error reporting. @@ -1088,7 +1069,7 @@ private async Task GenerateLuaMetaFileAsync(CancellationToken cancellationToken) "Moongate", _engineConfig.EngineVersion, _scriptModules, - new Dictionary(_constants), + new(_constants), manualModulesSnapshot, _nameResolver ); @@ -1122,7 +1103,7 @@ private string GenerateLuarcJson() var luarcConfig = new LuarcConfig { - Runtime = new LuarcRuntimeConfig + Runtime = new() { Path = [ @@ -1132,11 +1113,11 @@ private string GenerateLuarcJson() "modules/?/init.lua" ] }, - Workspace = new LuarcWorkspaceConfig + Workspace = new() { Library = [_engineConfig.ScriptsDirectory] }, - Diagnostics = new LuarcDiagnosticsConfig + Diagnostics = new() { Globals = [..globalsList] } @@ -1146,7 +1127,7 @@ private string GenerateLuarcJson() } /// - /// Generates a hash for script caching + /// Generates a hash for script caching /// private static string GetScriptHash(string script) { @@ -1156,9 +1137,7 @@ private static string GetScriptHash(string script) } private static bool IsSimpleType(Type type) - { - return type.IsPrimitive || type == typeof(string) || type.IsEnum; - } + => type.IsPrimitive || type == typeof(string) || type.IsEnum; private void LoadToUserData() { @@ -1274,7 +1253,7 @@ string functionName } else { - moduleTable = new Table(LuaScript); + moduleTable = new(LuaScript); LuaScript.Globals[normalizedModuleName] = moduleTable; } @@ -1322,7 +1301,8 @@ private void RegisterEnum(Type enumType) var metatable = new Table(LuaScript); // __index: allows case-insensitive access - metatable["__index"] = DynValue.NewCallback((ctx, args) => + metatable["__index"] = DynValue.NewCallback( + (ctx, args) => { var key = args[1].String; @@ -1356,7 +1336,8 @@ private void RegisterEnum(Type enumType) ); // __newindex: prevents modifications (read-only) - metatable["__newindex"] = DynValue.NewCallback((ctx, args) => + metatable["__newindex"] = DynValue.NewCallback( + (ctx, args) => { var key = args[1].String; @@ -1365,8 +1346,7 @@ private void RegisterEnum(Type enumType) ); // __tostring: pretty print - metatable["__tostring"] = DynValue.NewCallback((ctx, args) => { return DynValue.NewString($"enum<{enumName}>"); } - ); + metatable["__tostring"] = DynValue.NewCallback((ctx, args) => { return DynValue.NewString($"enum<{enumName}>"); }); // Set the enum table first var enumTableDynValue = DynValue.NewTable(enumTable); @@ -1407,9 +1387,9 @@ private void RegisterEnums() private void RegisterGlobalFunctions() { LuaScript.Globals["delay"] = (Func)(async milliseconds => - { - await Task.Delay(Math.Min(milliseconds, 5000)); - }); + { + await Task.Delay(Math.Min(milliseconds, 5000)); + }); // NOTE: do NOT define a bare 'log' global here — the LogModule registered above already // exposes 'log.info / log.warning / log.error' as a table; overwriting it with a function @@ -1420,7 +1400,7 @@ private void RegisterGlobalFunctions() private void RegisterManualModuleFunction(string moduleName, string functionName) { - var functions = _manualModuleFunctions.GetOrAdd(moduleName, _ => new ConcurrentDictionary()); + var functions = _manualModuleFunctions.GetOrAdd(moduleName, _ => new()); functions.TryAdd(functionName, 0); } @@ -1466,7 +1446,7 @@ private async Task RegisterScriptModulesAsync(CancellationToken cancellationToke } /// - /// Disposes of the resources used by the LuaScriptEngineService. + /// Disposes of the resources used by the LuaScriptEngineService. /// public void Dispose() { @@ -1496,12 +1476,12 @@ public void Dispose() } /// - /// Raised when a watched Lua file changes. + /// Raised when a watched Lua file changes. /// public event IScriptEngineService.LuaFileChangedHandler? FileChanged; /// - /// Event raised when a script error occurs + /// Event raised when a script error occurs /// public event EventHandler? OnScriptError; diff --git a/src/SquidStd.Scripting.Lua/Utils/LuaDocumentationGenerator.cs b/src/SquidStd.Scripting.Lua/Utils/LuaDocumentationGenerator.cs index 92e25e5b..bc69e34c 100644 --- a/src/SquidStd.Scripting.Lua/Utils/LuaDocumentationGenerator.cs +++ b/src/SquidStd.Scripting.Lua/Utils/LuaDocumentationGenerator.cs @@ -11,8 +11,8 @@ namespace SquidStd.Scripting.Lua.Utils; /// -/// Utility class for generating Lua meta files with EmmyLua/LuaLS annotations -/// Automatically creates meta.lua files with function signatures, types, and documentation +/// Utility class for generating Lua meta files with EmmyLua/LuaLS annotations +/// Automatically creates meta.lua files with function signatures, types, and documentation /// [RequiresUnreferencedCode( "This class uses reflection to analyze types for Lua meta generation and requires full type metadata." @@ -30,12 +30,12 @@ public static class LuaDocumentationGenerator private static Func _nameResolver = name => name.ToSnakeCase(); /// - /// List of enums found during documentation generation + /// List of enums found during documentation generation /// public static List FoundEnums { get; } = new(16); /// - /// Adds a class type to be generated in the documentation + /// Adds a class type to be generated in the documentation /// public static void AddClassToGenerate(Type type) { @@ -44,7 +44,7 @@ public static void AddClassToGenerate(Type type) } /// - /// Clears all internal caches and state + /// Clears all internal caches and state /// public static void ClearCaches() { @@ -60,12 +60,13 @@ public static void ClearCaches() } } - [SuppressMessage("Trimming", "IL2075:Reflection", Justification = "Reflection is required for script module analysis")] - [SuppressMessage( - "Trimming", - "IL2072:Reflection", - Justification = "Reflection is required for parameter and return type analysis" - )] + [SuppressMessage("Trimming", "IL2075:Reflection", Justification = "Reflection is required for script module analysis"), + SuppressMessage( + "Trimming", + "IL2072:Reflection", + Justification = "Reflection is required for parameter and return type analysis" + )] + /// /// Generates Lua documentation meta file with all module functions, classes, and constants /// @@ -124,8 +125,8 @@ public static string GenerateDocumentation( } var distinctConstants = constants - .GroupBy(kvp => kvp.Key) - .ToDictionary(g => g.Key, g => g.First().Value); + .GroupBy(kvp => kvp.Key) + .ToDictionary(g => g.Key, g => g.First().Value); ProcessConstants(distinctConstants); sb.Append(_constantsBuilder); @@ -156,9 +157,9 @@ public static string GenerateDocumentation( // Get all methods with ScriptFunction attribute var methods = module.ModuleType - .GetMethods(BindingFlags.Public | BindingFlags.Instance) - .Where(m => m.GetCustomAttribute() is not null) - .ToList(); + .GetMethods(BindingFlags.Public | BindingFlags.Instance) + .Where(m => m.GetCustomAttribute() is not null) + .ToList(); foreach (var method in methods) { @@ -170,8 +171,8 @@ public static string GenerateDocumentation( } var functionName = string.IsNullOrWhiteSpace(scriptFunctionAttr.FunctionName) - ? _nameResolver(method.Name) - : scriptFunctionAttr.FunctionName; + ? _nameResolver(method.Name) + : scriptFunctionAttr.FunctionName; sb.AppendLine(CultureInfo.InvariantCulture, $"{moduleName}.{functionName} = function() end"); } @@ -190,8 +191,8 @@ public static string GenerateDocumentation( } var functionName = string.IsNullOrWhiteSpace(scriptFunctionAttr.FunctionName) - ? _nameResolver(method.Name) - : scriptFunctionAttr.FunctionName; + ? _nameResolver(method.Name) + : scriptFunctionAttr.FunctionName; var description = scriptFunctionAttr.HelpText ?? "No description available"; sb.AppendLine("---"); @@ -205,8 +206,8 @@ public static string GenerateDocumentation( { var isParams = param.IsDefined(typeof(ParamArrayAttribute), false); var paramType = isParams - ? ConvertToLuaType(param.ParameterType.GetElementType()!) - : ConvertToLuaType(param.ParameterType); + ? ConvertToLuaType(param.ParameterType.GetElementType()!) + : ConvertToLuaType(param.ParameterType); var paramName = isParams ? "..." : param.Name ?? $"param{Array.IndexOf(parameters, param)}"; var paramDescription = GetParameterDescription(param, paramType); sb.AppendLine( @@ -294,13 +295,11 @@ public static string GenerateDocumentation( } private static bool CanProcessType(Type type) - { - return true; - } + => true; - [SuppressMessage("Trimming", "IL2070:Reflection", Justification = "Reflection is required for Lua type conversion")] - [SuppressMessage("Trimming", "IL2072:Reflection", Justification = "Reflection is required for Lua type conversion")] - [SuppressMessage("Trimming", "IL2062:Reflection", Justification = "Reflection is required for Lua type conversion")] + [SuppressMessage("Trimming", "IL2070:Reflection", Justification = "Reflection is required for Lua type conversion"), + SuppressMessage("Trimming", "IL2072:Reflection", Justification = "Reflection is required for Lua type conversion"), + SuppressMessage("Trimming", "IL2062:Reflection", Justification = "Reflection is required for Lua type conversion")] private static string ConvertToLuaType( [DynamicallyAccessedMembers( DynamicallyAccessedMemberTypes.PublicMethods | @@ -585,7 +584,7 @@ private static string FormatConstantValue(object? value, Type type) } /// - /// Generate all classes after collecting them, with robust circular dependency handling + /// Generate all classes after collecting them, with robust circular dependency handling /// private static void GenerateAllClasses() { @@ -635,10 +634,10 @@ private static void GenerateAllClasses() } /// - /// Generate a single class with properties, constructors, and methods + /// Generate a single class with properties, constructors, and methods /// - [SuppressMessage("Trimming", "IL2070:Reflection", Justification = "Reflection is required for class generation")] - [SuppressMessage("Trimming", "IL2072:Reflection", Justification = "Reflection is required for class generation")] + [SuppressMessage("Trimming", "IL2070:Reflection", Justification = "Reflection is required for class generation"), + SuppressMessage("Trimming", "IL2072:Reflection", Justification = "Reflection is required for class generation")] private static void GenerateClass( [DynamicallyAccessedMembers( DynamicallyAccessedMemberTypes.PublicProperties | @@ -674,8 +673,8 @@ Type type // Generate properties with documentation (exclude indexers which have parameters) var properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance) - .Where(p => p.CanRead && p.GetIndexParameters().Length == 0) - .ToList(); + .Where(p => p.CanRead && p.GetIndexParameters().Length == 0) + .ToList(); foreach (var property in properties) { @@ -694,8 +693,8 @@ Type type // Use original property name for built-in types (XNA), apply resolver for custom types var displayName = luaFieldAttr?.Name ?? (type.Namespace?.StartsWith("Microsoft.Xna.Framework", StringComparison.Ordinal) == true - ? propertyName - : _nameResolver(propertyName)); + ? propertyName + : _nameResolver(propertyName)); _classesBuilder.AppendLine( CultureInfo.InvariantCulture, @@ -705,8 +704,8 @@ Type type // Generate public constructors var constructors = type.GetConstructors(BindingFlags.Public) - .Where(c => c.GetParameters().Length > 0) - .ToList(); + .Where(c => c.GetParameters().Length > 0) + .ToList(); if (constructors.Count > 0) { @@ -729,8 +728,8 @@ Type type // Use original parameter names for XNA types, apply resolver for custom types var paramName = isXnaType - ? param.Name ?? $"param{i}" - : _nameResolver(param.Name ?? $"param{i}"); + ? param.Name ?? $"param{i}" + : _nameResolver(param.Name ?? $"param{i}"); _classesBuilder.Append(CultureInfo.InvariantCulture, $"{paramName}: {paramType}"); if (i < parameters.Length - 1) @@ -770,11 +769,12 @@ Type type }; var methods = type.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly) - .Where(m => !propertyMethods.Contains(m.Name) && - !m.IsSpecialName && - !excludedMethods.Contains(m.Name) - ) - .ToList(); + .Where( + m => !propertyMethods.Contains(m.Name) && + !m.IsSpecialName && + !excludedMethods.Contains(m.Name) + ) + .ToList(); if (methods.Count > 0) { @@ -794,8 +794,8 @@ Type type foreach (var method in methodGroup) { var returnType = method.ReturnType == typeof(void) - ? "nil" - : ConvertToLuaType(method.ReturnType); + ? "nil" + : ConvertToLuaType(method.ReturnType); var parameters = method.GetParameters(); @@ -808,8 +808,8 @@ Type type // Use original parameter names for XNA types, apply resolver for custom types var paramName = isXnaType - ? param.Name ?? $"param{i}" - : _nameResolver(param.Name ?? $"param{i}"); + ? param.Name ?? $"param{i}" + : _nameResolver(param.Name ?? $"param{i}"); _classesBuilder.Append(CultureInfo.InvariantCulture, $"{paramName}: {paramType}"); if (i < parameters.Length - 1) @@ -885,7 +885,7 @@ private static void GenerateEnumClass(Type enumType) } /// - /// Gets enhanced parameter description with type information + /// Gets enhanced parameter description with type information /// private static string GetParameterDescription(ParameterInfo param, string luaType) { @@ -933,7 +933,7 @@ private static string GetParameterDescription(ParameterInfo param, string luaTyp } /// - /// Gets enhanced return description with type information + /// Gets enhanced return description with type information /// private static string GetReturnDescription(Type returnType, string luaType) { @@ -968,7 +968,7 @@ private static string GetReturnDescription(Type returnType, string luaType) } /// - /// Check if a type is a C# record type + /// Check if a type is a C# record type /// [SuppressMessage("Trimming", "IL2070:Reflection", Justification = "Reflection is required for record type detection")] private static bool IsRecordType( @@ -1005,10 +1005,11 @@ Type type } var methods = type.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly); - var hasCompilerGeneratedToString = methods.Any(m => - m.Name == "ToString" && - m.GetParameters().Length == 0 && - m.GetCustomAttributes().Any(attr => attr.GetType().Name.Contains("CompilerGenerated")) + var hasCompilerGeneratedToString = methods.Any( + m => + m.Name == "ToString" && + m.GetParameters().Length == 0 && + m.GetCustomAttributes().Any(attr => attr.GetType().Name.Contains("CompilerGenerated")) ); var result = hasCompilerGeneratedToString; diff --git a/src/SquidStd.Search.Abstractions/Attributes/SearchIndexAttribute.cs b/src/SquidStd.Search.Abstractions/Attributes/SearchIndexAttribute.cs index 1ea23c62..f1c689ec 100644 --- a/src/SquidStd.Search.Abstractions/Attributes/SearchIndexAttribute.cs +++ b/src/SquidStd.Search.Abstractions/Attributes/SearchIndexAttribute.cs @@ -1,8 +1,8 @@ namespace SquidStd.Search.Abstractions.Attributes; /// -/// Declares the Elasticsearch index for a type. The name supports environment-variable expansion: -/// ${VAR} and ${VAR:-default} (e.g. "orders_${ENV:-dev}"). +/// Declares the Elasticsearch index for a type. The name supports environment-variable expansion: +/// ${VAR} and ${VAR:-default} (e.g. "orders_${ENV:-dev}"). /// [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, Inherited = false)] public sealed class SearchIndexAttribute : Attribute diff --git a/src/SquidStd.Search.Abstractions/Exceptions/SearchException.cs b/src/SquidStd.Search.Abstractions/Exceptions/SearchException.cs index d82f80a0..69b187f7 100644 --- a/src/SquidStd.Search.Abstractions/Exceptions/SearchException.cs +++ b/src/SquidStd.Search.Abstractions/Exceptions/SearchException.cs @@ -5,13 +5,9 @@ public sealed class SearchException : Exception { /// Initializes the exception with a message. public SearchException(string message) - : base(message) - { - } + : base(message) { } /// Initializes the exception with a message and inner exception. public SearchException(string message, Exception innerException) - : base(message, innerException) - { - } + : base(message, innerException) { } } diff --git a/src/SquidStd.Search.Abstractions/Interfaces/IIndexableEntity.cs b/src/SquidStd.Search.Abstractions/Interfaces/IIndexableEntity.cs index 80be32f4..4624c8a2 100644 --- a/src/SquidStd.Search.Abstractions/Interfaces/IIndexableEntity.cs +++ b/src/SquidStd.Search.Abstractions/Interfaces/IIndexableEntity.cs @@ -1,8 +1,8 @@ namespace SquidStd.Search.Abstractions.Interfaces; /// -/// Marks an entity as indexable and supplies its document id. The target index comes from a -/// on the type (or the lowercased type name). +/// Marks an entity as indexable and supplies its document id. The target index comes from a +/// on the type (or the lowercased type name). /// public interface IIndexableEntity { diff --git a/src/SquidStd.Search.Abstractions/Interfaces/ISearchService.cs b/src/SquidStd.Search.Abstractions/Interfaces/ISearchService.cs index b7f9c6b4..be94562d 100644 --- a/src/SquidStd.Search.Abstractions/Interfaces/ISearchService.cs +++ b/src/SquidStd.Search.Abstractions/Interfaces/ISearchService.cs @@ -1,7 +1,7 @@ namespace SquidStd.Search.Abstractions.Interfaces; /// -/// Indexes, deletes, and queries documents. +/// Indexes, deletes, and queries documents. /// public interface ISearchService { diff --git a/src/SquidStd.Search.Abstractions/Search/SearchIndexNameResolver.cs b/src/SquidStd.Search.Abstractions/Search/SearchIndexNameResolver.cs index febd3317..1a8b04f6 100644 --- a/src/SquidStd.Search.Abstractions/Search/SearchIndexNameResolver.cs +++ b/src/SquidStd.Search.Abstractions/Search/SearchIndexNameResolver.cs @@ -4,9 +4,9 @@ namespace SquidStd.Search.Abstractions.Search; /// -/// Resolves the Elasticsearch index name for a type from its (or the -/// lowercased type name), expanding ${VAR} / ${VAR:-default} from environment variables. The -/// result is always lowercased (an Elasticsearch requirement). +/// Resolves the Elasticsearch index name for a type from its (or the +/// lowercased type name), expanding ${VAR} / ${VAR:-default} from environment variables. The +/// result is always lowercased (an Elasticsearch requirement). /// public static partial class SearchIndexNameResolver { @@ -17,15 +17,14 @@ public static string Resolve(Type type) var template = type.GetCustomAttributes(typeof(SearchIndexAttribute), false) is { Length: > 0 } attributes && attributes[0] is SearchIndexAttribute attribute - ? attribute.Name - : type.Name; + ? attribute.Name + : type.Name; return ExpandEnvironment(template).ToLowerInvariant(); } private static string ExpandEnvironment(string template) - { - return PlaceholderRegex() + => PlaceholderRegex() .Replace( template, match => @@ -49,7 +48,6 @@ private static string ExpandEnvironment(string template) ); } ); - } [GeneratedRegex(@"\$\{(?[A-Za-z_][A-Za-z0-9_]*)(:-(?[^}]*))?\}")] private static partial Regex PlaceholderRegex(); diff --git a/src/SquidStd.Search.Elasticsearch/Extensions/SearchRegistrationExtensions.cs b/src/SquidStd.Search.Elasticsearch/Extensions/SearchRegistrationExtensions.cs index b5b5934a..94ef2f2a 100644 --- a/src/SquidStd.Search.Elasticsearch/Extensions/SearchRegistrationExtensions.cs +++ b/src/SquidStd.Search.Elasticsearch/Extensions/SearchRegistrationExtensions.cs @@ -28,7 +28,7 @@ private static ElasticsearchClient CreateClient(ElasticsearchOptions options) settings = settings.ServerCertificateValidationCallback((_, _, _, _) => true); } - return new ElasticsearchClient(settings); + return new(settings); } extension(IContainer container) diff --git a/src/SquidStd.Search.Elasticsearch/Linq/ElasticExpressionTranslator.cs b/src/SquidStd.Search.Elasticsearch/Linq/ElasticExpressionTranslator.cs index 60aa43d9..0eff7e6f 100644 --- a/src/SquidStd.Search.Elasticsearch/Linq/ElasticExpressionTranslator.cs +++ b/src/SquidStd.Search.Elasticsearch/Linq/ElasticExpressionTranslator.cs @@ -5,10 +5,10 @@ namespace SquidStd.Search.Elasticsearch.Linq; /// -/// Translates a constrained LINQ expression chain into an . Supported: Where -/// (==, !=, <, >, <=, >=, &&, ||, !, bool members, string.Contains/StartsWith), -/// OrderBy/ThenBy(Descending), Skip, Take, and the Match/FullText markers. Anything else throws -/// . +/// Translates a constrained LINQ expression chain into an . Supported: Where +/// (==, !=, <, >, <=, >=, &&, ||, !, bool members, string.Contains/StartsWith), +/// OrderBy/ThenBy(Descending), Skip, Take, and the Match/FullText markers. Anything else throws +/// . /// public static class ElasticExpressionTranslator { @@ -25,7 +25,7 @@ public static ElasticQuery Translate(Expression expression, Type elementType) if (must.Count > 0) { - result.Query = new JsonObject { ["bool"] = new JsonObject { ["must"] = must } }; + result.Query = new() { ["bool"] = new JsonObject { ["must"] = must } }; } if (sort.Count > 0) @@ -37,11 +37,9 @@ public static ElasticQuery Translate(Expression expression, Type elementType) } private static object? EvaluateValue(Expression expression) - { - return expression is ConstantExpression constant - ? constant.Value - : Expression.Lambda(expression).Compile().DynamicInvoke(); - } + => expression is ConstantExpression constant + ? constant.Value + : Expression.Lambda(expression).Compile().DynamicInvoke(); private static string FieldName(MemberExpression member) { @@ -58,19 +56,15 @@ private static string FieldName(MemberExpression member) } private static object? GetConstant(Expression expression) - { - return EvaluateValue(expression); - } + => EvaluateValue(expression); private static bool IsParameterBound(Expression expression) - { - return expression switch + => expression switch { ParameterExpression => true, MemberExpression m => m.Expression is not null && IsParameterBound(m.Expression), _ => false }; - } private static (MemberExpression Member, Expression Value) OrientMemberValue(BinaryExpression binary) { @@ -88,33 +82,27 @@ private static (MemberExpression Member, Expression Value) OrientMemberValue(Bin } private static JsonObject Range(string field, string op, JsonNode? value) - { - return new JsonObject { ["range"] = new JsonObject { [field] = new JsonObject { [op] = value } } }; - } + => new() { ["range"] = new JsonObject { [field] = new JsonObject { [op] = value } } }; private static JsonObject SortClause(Expression keySelector, string order) { var member = (MemberExpression)UnquoteLambda(keySelector).Body; - return new JsonObject { [FieldName(member)] = new JsonObject { ["order"] = order } }; + return new() { [FieldName(member)] = new JsonObject { ["order"] = order } }; } private static Expression StripConvert(Expression expression) - { - return expression is UnaryExpression { NodeType: ExpressionType.Convert } convert ? convert.Operand : expression; - } + => expression is UnaryExpression { NodeType: ExpressionType.Convert } convert ? convert.Operand : expression; private static JsonObject TermOrKeyword(string field, Type memberType, JsonNode? value) { var termField = memberType == typeof(string) ? $"{field}.keyword" : field; - return new JsonObject { ["term"] = new JsonObject { [termField] = value } }; + return new() { ["term"] = new JsonObject { [termField] = value } }; } private static JsonNode? ToJsonValue(object? value) - { - return value is null ? null : JsonSerializer.SerializeToNode(value, WebOptions); - } + => value is null ? null : JsonSerializer.SerializeToNode(value, WebOptions); private static JsonObject TranslateComparison(BinaryExpression binary) { @@ -125,7 +113,7 @@ private static JsonObject TranslateComparison(BinaryExpression binary) return binary.NodeType switch { ExpressionType.Equal => TermOrKeyword(field, member.Type, value), - ExpressionType.NotEqual => new JsonObject + ExpressionType.NotEqual => new() { ["bool"] = new JsonObject { ["must_not"] = new JsonArray(TermOrKeyword(field, member.Type, value)) } }, ExpressionType.GreaterThan => Range(field, "gt", value), ExpressionType.GreaterThanOrEqual => Range(field, "gte", value), @@ -140,13 +128,13 @@ private static JsonObject TranslatePredicate(Expression expression) switch (expression) { case BinaryExpression { NodeType: ExpressionType.AndAlso } and1: - return new JsonObject + return new() { ["bool"] = new JsonObject { ["must"] = new JsonArray(TranslatePredicate(and1.Left), TranslatePredicate(and1.Right)) } }; case BinaryExpression { NodeType: ExpressionType.OrElse } or1: - return new JsonObject + return new() { ["bool"] = new JsonObject { @@ -155,12 +143,11 @@ private static JsonObject TranslatePredicate(Expression expression) } }; case UnaryExpression { NodeType: ExpressionType.Not } not: - return new JsonObject - { ["bool"] = new JsonObject { ["must_not"] = new JsonArray(TranslatePredicate(not.Operand)) } }; + return new() { ["bool"] = new JsonObject { ["must_not"] = new JsonArray(TranslatePredicate(not.Operand)) } }; case BinaryExpression binary: return TranslateComparison(binary); case MemberExpression member when member.Type == typeof(bool): - return new JsonObject { ["term"] = new JsonObject { [FieldName(member)] = true } }; + return new() { ["term"] = new JsonObject { [FieldName(member)] = true } }; case MethodCallExpression methodCall: return TranslateStringMethod(methodCall); default: @@ -180,25 +167,21 @@ private static JsonObject TranslateStringMethod(MethodCallExpression call) return call.Method.Name switch { - "Contains" => new JsonObject { ["wildcard"] = new JsonObject { [$"{field}.keyword"] = $"*{arg}*" } }, - "StartsWith" => new JsonObject { ["prefix"] = new JsonObject { [$"{field}.keyword"] = arg } }, + "Contains" => new() { ["wildcard"] = new JsonObject { [$"{field}.keyword"] = $"*{arg}*" } }, + "StartsWith" => new() { ["prefix"] = new JsonObject { [$"{field}.keyword"] = arg } }, _ => throw Unsupported(call) }; } private static LambdaExpression UnquoteLambda(Expression expression) - { - return expression is UnaryExpression { NodeType: ExpressionType.Quote } quote - ? (LambdaExpression)quote.Operand - : (LambdaExpression)expression; - } + => expression is UnaryExpression { NodeType: ExpressionType.Quote } quote + ? (LambdaExpression)quote.Operand + : (LambdaExpression)expression; private static NotSupportedException Unsupported(Expression expression) - { - return new NotSupportedException( + => new( $"Expression '{expression}' is not supported by the Elasticsearch provider. Use the native ElasticsearchClient for advanced queries." ); - } private static void Walk(Expression expression, JsonArray must, JsonArray sort, ElasticQuery result) { diff --git a/src/SquidStd.Search.Elasticsearch/Linq/ElasticQueryProvider.cs b/src/SquidStd.Search.Elasticsearch/Linq/ElasticQueryProvider.cs index 65f5c23e..c9eb475d 100644 --- a/src/SquidStd.Search.Elasticsearch/Linq/ElasticQueryProvider.cs +++ b/src/SquidStd.Search.Elasticsearch/Linq/ElasticQueryProvider.cs @@ -6,8 +6,8 @@ namespace SquidStd.Search.Elasticsearch.Linq; /// -/// Executes a translated against Elasticsearch. Synchronous LINQ execution is not -/// supported — use the async terminals in . +/// Executes a translated against Elasticsearch. Synchronous LINQ execution is not +/// supported — use the async terminals in . /// public sealed class ElasticQueryProvider : IQueryProvider { @@ -23,28 +23,20 @@ public ElasticQueryProvider(ElasticTransport transport, string index, Type eleme } public IQueryable CreateQuery(Expression expression) - { - return (IQueryable)Activator.CreateInstance( + => (IQueryable)Activator.CreateInstance( typeof(ElasticQueryable<>).MakeGenericType(_elementType), this, expression )!; - } public IQueryable CreateQuery(Expression expression) - { - return new ElasticQueryable(this, expression); - } + => new ElasticQueryable(this, expression); public object? Execute(Expression expression) - { - throw new NotSupportedException("Use the async terminals (ToListAsync/CountAsync/FirstOrDefaultAsync)."); - } + => throw new NotSupportedException("Use the async terminals (ToListAsync/CountAsync/FirstOrDefaultAsync)."); public TResult Execute(Expression expression) - { - throw new NotSupportedException("Use the async terminals (ToListAsync/CountAsync/FirstOrDefaultAsync)."); - } + => throw new NotSupportedException("Use the async terminals (ToListAsync/CountAsync/FirstOrDefaultAsync)."); /// Runs a count and returns the total. public async Task CountAsync(Expression expression, CancellationToken cancellationToken) @@ -61,11 +53,11 @@ public async Task> ToListAsync(Expression expression, CancellationTok { var query = ElasticExpressionTranslator.Translate(expression, _elementType); var (status, body) = await _transport.SendAsync( - HttpMethod.POST, - $"/{_index}/_search", - query.ToRequestBody(), - cancellationToken - ); + HttpMethod.POST, + $"/{_index}/_search", + query.ToRequestBody(), + cancellationToken + ); if (status == 404) { diff --git a/src/SquidStd.Search.Elasticsearch/Linq/ElasticQueryable.cs b/src/SquidStd.Search.Elasticsearch/Linq/ElasticQueryable.cs index 5ce839f5..4ab05700 100644 --- a/src/SquidStd.Search.Elasticsearch/Linq/ElasticQueryable.cs +++ b/src/SquidStd.Search.Elasticsearch/Linq/ElasticQueryable.cs @@ -25,12 +25,8 @@ public ElasticQueryable(ElasticQueryProvider provider) } public IEnumerator GetEnumerator() - { - throw new NotSupportedException("Use the async terminals (ToListAsync/CountAsync/FirstOrDefaultAsync)."); - } + => throw new NotSupportedException("Use the async terminals (ToListAsync/CountAsync/FirstOrDefaultAsync)."); IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } + => GetEnumerator(); } diff --git a/src/SquidStd.Search.Elasticsearch/Linq/ElasticQueryableExtensions.cs b/src/SquidStd.Search.Elasticsearch/Linq/ElasticQueryableExtensions.cs index f2081720..9373c06a 100644 --- a/src/SquidStd.Search.Elasticsearch/Linq/ElasticQueryableExtensions.cs +++ b/src/SquidStd.Search.Elasticsearch/Linq/ElasticQueryableExtensions.cs @@ -4,31 +4,23 @@ namespace SquidStd.Search.Elasticsearch.Linq; /// -/// LINQ surface for the Elasticsearch provider. and are -/// markers recognized by the translator (no standalone runtime behavior); the async terminals execute the query. +/// LINQ surface for the Elasticsearch provider. and are +/// markers recognized by the translator (no standalone runtime behavior); the async terminals execute the query. /// public static class ElasticQueryableExtensions { private static ElasticQueryProvider Provider(IQueryable source) - { - return source.Provider as ElasticQueryProvider ?? - throw new NotSupportedException( - "These async terminals require a query created by ISearchService.Query()." - ); - } + => source.Provider as ElasticQueryProvider ?? + throw new NotSupportedException("These async terminals require a query created by ISearchService.Query()."); extension(IQueryable source) { /// Executes a count of matching documents. public Task CountAsync(CancellationToken cancellationToken = default) - { - return Provider(source).CountAsync(source.Expression, cancellationToken); - } + => Provider(source).CountAsync(source.Expression, cancellationToken); /// Executes the query and returns the first matching document, or null. - public async Task FirstOrDefaultAsync( - CancellationToken cancellationToken = default - ) + public async Task FirstOrDefaultAsync(CancellationToken cancellationToken = default) { var limited = source.Take(1); var results = await Provider(limited).ToListAsync(limited.Expression, cancellationToken); @@ -38,20 +30,17 @@ public Task CountAsync(CancellationToken cancellationToken = default) /// Full-text match of across all fields. public IQueryable FullText(string text) - { - return source.Provider.CreateQuery( + => source.Provider.CreateQuery( Expression.Call( ((MethodInfo)MethodBase.GetCurrentMethod()!).MakeGenericMethod(typeof(T)), source.Expression, Expression.Constant(text) ) ); - } /// Full-text match of against a single field. public IQueryable Match(string field, string text) - { - return source.Provider.CreateQuery( + => source.Provider.CreateQuery( Expression.Call( ((MethodInfo)MethodBase.GetCurrentMethod()!).MakeGenericMethod(typeof(T)), source.Expression, @@ -59,12 +48,9 @@ public IQueryable Match(string field, string text) Expression.Constant(text) ) ); - } /// Executes the query and returns all matching documents. public Task> ToListAsync(CancellationToken cancellationToken = default) - { - return Provider(source).ToListAsync(source.Expression, cancellationToken); - } + => Provider(source).ToListAsync(source.Expression, cancellationToken); } } diff --git a/src/SquidStd.Search.Elasticsearch/README.md b/src/SquidStd.Search.Elasticsearch/README.md index 2b80206a..e1c668f1 100644 --- a/src/SquidStd.Search.Elasticsearch/README.md +++ b/src/SquidStd.Search.Elasticsearch/README.md @@ -37,12 +37,12 @@ Anything else throws `NotSupportedException` — drop down to the native `Elasti ## Key types -| Type | Purpose | -|------|---------| -| `SearchRegistrationExtensions` | `AddElasticsearch(...)` registration. | -| `ElasticSearchService` | `ISearchService` backed by the Elasticsearch client. | -| `ElasticsearchOptions` | Connection options (URI, credentials). | -| `ElasticQueryable` | Constrained `IQueryable` translated to the Elasticsearch query DSL. | +| Type | Purpose | +|--------------------------------|------------------------------------------------------------------------| +| `SearchRegistrationExtensions` | `AddElasticsearch(...)` registration. | +| `ElasticSearchService` | `ISearchService` backed by the Elasticsearch client. | +| `ElasticsearchOptions` | Connection options (URI, credentials). | +| `ElasticQueryable` | Constrained `IQueryable` translated to the Elasticsearch query DSL. | ## Related diff --git a/src/SquidStd.Search.Elasticsearch/Services/ElasticSearchService.cs b/src/SquidStd.Search.Elasticsearch/Services/ElasticSearchService.cs index d74c3d4f..afff670f 100644 --- a/src/SquidStd.Search.Elasticsearch/Services/ElasticSearchService.cs +++ b/src/SquidStd.Search.Elasticsearch/Services/ElasticSearchService.cs @@ -11,8 +11,8 @@ namespace SquidStd.Search.Elasticsearch.Services; /// -/// Default : indexes, deletes, and queries documents over the Elasticsearch -/// low-level transport, with index names resolved from [SearchIndex] + the configured prefix. +/// Default : indexes, deletes, and queries documents over the Elasticsearch +/// low-level transport, with index names resolved from [SearchIndex] + the configured prefix. /// public sealed class ElasticSearchService : ISearchService { @@ -82,11 +82,11 @@ public async Task IndexAsync(T entity, bool refresh = false, CancellationToke var index = ResolveIndex(); var path = $"/{index}/_doc/{Uri.EscapeDataString(entity.IndexId)}{RefreshQuery(refresh)}"; var (status, body) = await _transport.SendAsync( - HttpMethod.PUT, - path, - ElasticTransport.SerializeDocument(entity), - cancellationToken - ); + HttpMethod.PUT, + path, + ElasticTransport.SerializeDocument(entity), + cancellationToken + ); EnsureSuccess(status, body, $"index document '{entity.IndexId}' into '{index}'"); } @@ -130,9 +130,7 @@ public async Task IndexManyAsync( /// public IQueryable Query() where T : IIndexableEntity - { - return new ElasticQueryable(new ElasticQueryProvider(_transport, ResolveIndex(), typeof(T))); - } + => new ElasticQueryable(new(_transport, ResolveIndex(), typeof(T))); /// Resolves the (prefixed, lowercased) index name for a type. public string ResolveIndex() @@ -156,7 +154,5 @@ private void EnsureSuccess(int status, JsonNode? body, string operation) } private static string RefreshQuery(bool refresh) - { - return refresh ? "?refresh=wait_for" : string.Empty; - } + => refresh ? "?refresh=wait_for" : string.Empty; } diff --git a/src/SquidStd.Search.Elasticsearch/Services/ElasticTransport.cs b/src/SquidStd.Search.Elasticsearch/Services/ElasticTransport.cs index 70e81ecf..3ab5c941 100644 --- a/src/SquidStd.Search.Elasticsearch/Services/ElasticTransport.cs +++ b/src/SquidStd.Search.Elasticsearch/Services/ElasticTransport.cs @@ -7,8 +7,8 @@ namespace SquidStd.Search.Elasticsearch.Services; /// -/// Thin JSON request helper over the Elasticsearch client's low-level transport. Sends raw DSL/document JSON -/// and returns the parsed response, decoupling the provider from the strongly-typed query DSL. +/// Thin JSON request helper over the Elasticsearch client's low-level transport. Sends raw DSL/document JSON +/// and returns the parsed response, decoupling the provider from the strongly-typed query DSL. /// public sealed class ElasticTransport { @@ -37,9 +37,7 @@ public ElasticTransport(ElasticsearchClient client) /// Deserializes a document body (an Elasticsearch _source) to . public static T DeserializeDocument(JsonNode source) - { - return source.Deserialize(WebOptions)!; - } + => source.Deserialize(WebOptions)!; /// Sends a request with an optional JSON body and returns (statusCode, bodyJson). public Task<(int Status, JsonNode? Body)> SendAsync( @@ -48,9 +46,7 @@ public static T DeserializeDocument(JsonNode source) JsonNode? body, CancellationToken cancellationToken ) - { - return SendCoreAsync(method, path, body?.ToJsonString(), JsonRequest, cancellationToken); - } + => SendCoreAsync(method, path, body?.ToJsonString(), JsonRequest, cancellationToken); /// Sends a raw (already-serialized) NDJSON body for the bulk API. public Task<(int Status, JsonNode? Body)> SendRawAsync( @@ -59,15 +55,11 @@ CancellationToken cancellationToken string? body, CancellationToken cancellationToken ) - { - return SendCoreAsync(method, path, body, NdjsonRequest, cancellationToken); - } + => SendCoreAsync(method, path, body, NdjsonRequest, cancellationToken); /// Serializes a value to a using Web (camelCase) defaults. public static JsonNode SerializeDocument(T value) - { - return JsonSerializer.SerializeToNode(value, WebOptions)!; - } + => JsonSerializer.SerializeToNode(value, WebOptions)!; private async Task<(int Status, JsonNode? Body)> SendCoreAsync( HttpMethod method, @@ -80,14 +72,14 @@ CancellationToken cancellationToken var postData = body is null ? null : PostData.String(body); var response = await _client.Transport - .RequestAsync( - method, - path, - postData, - requestConfiguration, - cancellationToken - ) - .ConfigureAwait(false); + .RequestAsync( + method, + path, + postData, + requestConfiguration, + cancellationToken + ) + .ConfigureAwait(false); var status = response.ApiCallDetails.HttpStatusCode ?? 0; var text = response.Body; diff --git a/src/SquidStd.Secrets.Aws/Internal/AwsClientFactory.cs b/src/SquidStd.Secrets.Aws/Internal/AwsClientFactory.cs index eaaf6c8c..954460e2 100644 --- a/src/SquidStd.Secrets.Aws/Internal/AwsClientFactory.cs +++ b/src/SquidStd.Secrets.Aws/Internal/AwsClientFactory.cs @@ -14,8 +14,8 @@ public static AWSCredentials Credentials(AwsConfigEntry aws) if (!string.IsNullOrWhiteSpace(aws.AccessKey) && !string.IsNullOrWhiteSpace(aws.SecretKey)) { return string.IsNullOrWhiteSpace(aws.SessionToken) - ? new BasicAWSCredentials(aws.AccessKey, aws.SecretKey) - : new SessionAWSCredentials(aws.AccessKey, aws.SecretKey, aws.SessionToken); + ? new BasicAWSCredentials(aws.AccessKey, aws.SecretKey) + : new SessionAWSCredentials(aws.AccessKey, aws.SecretKey, aws.SessionToken); } return FallbackCredentialsFactory.GetCredentials(); diff --git a/src/SquidStd.Secrets.Aws/README.md b/src/SquidStd.Secrets.Aws/README.md index bbc215d0..81544369 100644 --- a/src/SquidStd.Secrets.Aws/README.md +++ b/src/SquidStd.Secrets.Aws/README.md @@ -46,12 +46,12 @@ await foreach (var name in store.ListNamesAsync("db/")) { /* ... */ } ## Key types -| Type | Purpose | -|------|---------| -| `KmsSecretProtector` | `ISecretProtector` using AWS KMS data keys (envelope encryption). | -| `AwsSecretsManagerStore` | `ISecretStore` backed by AWS Secrets Manager. | -| `KmsSecretProtectorOptions` | KMS key id/ARN/alias, region and credentials. | -| `AwsSecretsManagerOptions` | Optional name prefix, region and credentials. | +| Type | Purpose | +|-----------------------------|-------------------------------------------------------------------| +| `KmsSecretProtector` | `ISecretProtector` using AWS KMS data keys (envelope encryption). | +| `AwsSecretsManagerStore` | `ISecretStore` backed by AWS Secrets Manager. | +| `KmsSecretProtectorOptions` | KMS key id/ARN/alias, region and credentials. | +| `AwsSecretsManagerOptions` | Optional name prefix, region and credentials. | ## Notes diff --git a/src/SquidStd.Secrets.Aws/Services/AwsSecretsManagerStore.cs b/src/SquidStd.Secrets.Aws/Services/AwsSecretsManagerStore.cs index f1f48c04..b3eedb91 100644 --- a/src/SquidStd.Secrets.Aws/Services/AwsSecretsManagerStore.cs +++ b/src/SquidStd.Secrets.Aws/Services/AwsSecretsManagerStore.cs @@ -18,7 +18,7 @@ public AwsSecretsManagerStore(AwsSecretsManagerOptions options) ArgumentNullException.ThrowIfNull(options); _prefix = options.NamePrefix ?? string.Empty; - _client = new AmazonSecretsManagerClient( + _client = new( AwsClientFactory.Credentials(options.Aws), AwsClientFactory.SecretsManagerConfig(options.Aws) ); @@ -37,10 +37,10 @@ public async ValueTask DeleteAsync(string name, CancellationToken cancella try { await _client.DeleteSecretAsync( - new DeleteSecretRequest { SecretId = _prefix + name, ForceDeleteWithoutRecovery = true }, - cancellationToken - ) - .ConfigureAwait(false); + new() { SecretId = _prefix + name, ForceDeleteWithoutRecovery = true }, + cancellationToken + ) + .ConfigureAwait(false); return true; } @@ -58,10 +58,10 @@ public async ValueTask ExistsAsync(string name, CancellationToken cancella try { await _client.DescribeSecretAsync( - new DescribeSecretRequest { SecretId = _prefix + name }, - cancellationToken - ) - .ConfigureAwait(false); + new() { SecretId = _prefix + name }, + cancellationToken + ) + .ConfigureAwait(false); return true; } @@ -79,10 +79,10 @@ await _client.DescribeSecretAsync( try { var response = await _client.GetSecretValueAsync( - new GetSecretValueRequest { SecretId = _prefix + name }, - cancellationToken - ) - .ConfigureAwait(false); + new() { SecretId = _prefix + name }, + cancellationToken + ) + .ConfigureAwait(false); return response.SecretString; } @@ -103,24 +103,25 @@ public async ValueTask SetAsync(string name, string value, CancellationToken can try { await _client.PutSecretValueAsync( - new PutSecretValueRequest { SecretId = secretId, SecretString = value }, - cancellationToken - ) - .ConfigureAwait(false); + new() { SecretId = secretId, SecretString = value }, + cancellationToken + ) + .ConfigureAwait(false); } catch (ResourceNotFoundException) { await _client.CreateSecretAsync( - new CreateSecretRequest { Name = secretId, SecretString = value }, - cancellationToken - ) - .ConfigureAwait(false); + new() { Name = secretId, SecretString = value }, + cancellationToken + ) + .ConfigureAwait(false); } } /// public async IAsyncEnumerable ListNamesAsync( - string? prefix = null, [EnumeratorCancellation] CancellationToken cancellationToken = default + string? prefix = null, + [EnumeratorCancellation] CancellationToken cancellationToken = default ) { var fullPrefix = _prefix + (prefix ?? string.Empty); @@ -129,10 +130,10 @@ public async IAsyncEnumerable ListNamesAsync( do { var response = await _client.ListSecretsAsync( - new ListSecretsRequest { NextToken = token, MaxResults = 100 }, - cancellationToken - ) - .ConfigureAwait(false); + new() { NextToken = token, MaxResults = 100 }, + cancellationToken + ) + .ConfigureAwait(false); foreach (var secret in response.SecretList) { @@ -148,7 +149,5 @@ public async IAsyncEnumerable ListNamesAsync( /// public void Dispose() - { - _client.Dispose(); - } + => _client.Dispose(); } diff --git a/src/SquidStd.Secrets.Aws/Services/KmsSecretProtector.cs b/src/SquidStd.Secrets.Aws/Services/KmsSecretProtector.cs index 32388dac..63be54ca 100644 --- a/src/SquidStd.Secrets.Aws/Services/KmsSecretProtector.cs +++ b/src/SquidStd.Secrets.Aws/Services/KmsSecretProtector.cs @@ -1,6 +1,5 @@ using System.Security.Cryptography; using Amazon.KeyManagementService; -using Amazon.KeyManagementService.Model; using SquidStd.Core.Interfaces.Secrets; using SquidStd.Secrets.Aws.Data; using SquidStd.Secrets.Aws.Internal; @@ -19,7 +18,7 @@ public KmsSecretProtector(KmsSecretProtectorOptions options) ArgumentException.ThrowIfNullOrWhiteSpace(options.KeyId); _keyId = options.KeyId; - _kms = new AmazonKeyManagementServiceClient( + _kms = new( AwsClientFactory.Credentials(options.Aws), AwsClientFactory.KmsConfig(options.Aws) ); @@ -30,11 +29,9 @@ public byte[] Protect(byte[] plaintext) { ArgumentNullException.ThrowIfNull(plaintext); - var generated = _kms.GenerateDataKeyAsync( - new GenerateDataKeyRequest { KeyId = _keyId, KeySpec = DataKeySpec.AES_256 } - ) - .GetAwaiter() - .GetResult(); + var generated = _kms.GenerateDataKeyAsync(new() { KeyId = _keyId, KeySpec = DataKeySpec.AES_256 }) + .GetAwaiter() + .GetResult(); var dataKey = generated.Plaintext.ToArray(); @@ -54,11 +51,9 @@ public byte[] Unprotect(byte[] protectedData) ArgumentNullException.ThrowIfNull(protectedData); var wrappedKey = KmsEnvelope.ReadWrappedKey(protectedData); - var decrypted = _kms.DecryptAsync( - new DecryptRequest { CiphertextBlob = new MemoryStream(wrappedKey) } - ) - .GetAwaiter() - .GetResult(); + var decrypted = _kms.DecryptAsync(new() { CiphertextBlob = new(wrappedKey) }) + .GetAwaiter() + .GetResult(); var dataKey = decrypted.Plaintext.ToArray(); @@ -74,7 +69,5 @@ public byte[] Unprotect(byte[] protectedData) /// public void Dispose() - { - _kms.Dispose(); - } + => _kms.Dispose(); } diff --git a/src/SquidStd.Services.Core/Extensions/Logger/SquidStdLogRollingIntervalExtensions.cs b/src/SquidStd.Services.Core/Extensions/Logger/SquidStdLogRollingIntervalExtensions.cs index bb9df9b9..79712f86 100644 --- a/src/SquidStd.Services.Core/Extensions/Logger/SquidStdLogRollingIntervalExtensions.cs +++ b/src/SquidStd.Services.Core/Extensions/Logger/SquidStdLogRollingIntervalExtensions.cs @@ -4,7 +4,7 @@ namespace SquidStd.Services.Core.Extensions.Logger; /// -/// Extension methods for converting SquidStd logger options to Serilog values. +/// Extension methods for converting SquidStd logger options to Serilog values. /// public static class SquidStdLogRollingIntervalExtensions { @@ -12,12 +12,11 @@ public static class SquidStdLogRollingIntervalExtensions extension(SquidStdLogRollingIntervalType interval) { /// - /// Converts a SquidStd rolling interval to a Serilog rolling interval. + /// Converts a SquidStd rolling interval to a Serilog rolling interval. /// /// The corresponding Serilog rolling interval. public RollingInterval ToSerilogRollingInterval() - { - return interval switch + => interval switch { SquidStdLogRollingIntervalType.Infinite => RollingInterval.Infinite, SquidStdLogRollingIntervalType.Year => RollingInterval.Year, @@ -27,6 +26,5 @@ public RollingInterval ToSerilogRollingInterval() SquidStdLogRollingIntervalType.Minute => RollingInterval.Minute, _ => RollingInterval.Day }; - } } } diff --git a/src/SquidStd.Services.Core/Extensions/RegisterCommandDispatcherExtensions.cs b/src/SquidStd.Services.Core/Extensions/RegisterCommandDispatcherExtensions.cs index 82fb76f4..de9e1436 100644 --- a/src/SquidStd.Services.Core/Extensions/RegisterCommandDispatcherExtensions.cs +++ b/src/SquidStd.Services.Core/Extensions/RegisterCommandDispatcherExtensions.cs @@ -6,7 +6,7 @@ namespace SquidStd.Services.Core.Extensions; /// -/// Extension methods for registering a command dispatcher and its context factory. +/// Extension methods for registering a command dispatcher and its context factory. /// public static class RegisterCommandDispatcherExtensions { @@ -14,7 +14,7 @@ public static class RegisterCommandDispatcherExtensions extension(IContainer container) { /// - /// Registers an singleton and its bootstrap activator. + /// Registers an singleton and its bootstrap activator. /// /// The dispatcher context type. /// The same container for chaining. @@ -27,9 +27,9 @@ public IContainer RegisterCommandDispatcher() } /// - /// Registers a seeded context factory and an - /// singleton over the existing (which must already be - /// registered via ). + /// Registers a seeded context factory and an + /// singleton over the existing (which must already be + /// registered via ). /// /// The dispatcher context type. /// The seed the context is built from. diff --git a/src/SquidStd.Services.Core/Extensions/RegisterDefaultServicesExtensions.cs b/src/SquidStd.Services.Core/Extensions/RegisterDefaultServicesExtensions.cs index 00ca09c7..c6c4bffc 100644 --- a/src/SquidStd.Services.Core/Extensions/RegisterDefaultServicesExtensions.cs +++ b/src/SquidStd.Services.Core/Extensions/RegisterDefaultServicesExtensions.cs @@ -28,7 +28,7 @@ namespace SquidStd.Services.Core.Extensions; /// -/// Extension methods for registering the default SquidStd core services. +/// Extension methods for registering the default SquidStd core services. /// public static class RegisterDefaultServicesExtensions { @@ -48,16 +48,14 @@ public IContainer RegisterConfigManagerService(string configName, string configD } /// - /// Registers the default SquidStd core services using the default config file location. + /// Registers the default SquidStd core services using the default config file location. /// /// The same container for chaining. public IContainer RegisterCoreServices() - { - return container.RegisterCoreServices("squidstd", Directory.GetCurrentDirectory()); - } + => container.RegisterCoreServices("squidstd", Directory.GetCurrentDirectory()); /// - /// Registers the default SquidStd core services and config manager. + /// Registers the default SquidStd core services and config manager. /// /// The logical config name or YAML file name. /// The directory where the config file is searched. @@ -79,14 +77,14 @@ public IContainer RegisterCoreServices(string configName, string configDirectory } /// - /// Registers the default config manager service as a singleton instance. + /// Registers the default config manager service as a singleton instance. /// /// The logical config name or YAML file name. /// The directory where the config file is searched. /// The same container for chaining. /// - /// Registers the default JSON data serializer for and - /// (same singleton instance). + /// Registers the default JSON data serializer for and + /// (same singleton instance). /// /// The same container for chaining. public IContainer RegisterDataSerializer() @@ -99,7 +97,7 @@ public IContainer RegisterDataSerializer() } /// - /// Registers the default SquidStd core configuration sections. + /// Registers the default SquidStd core configuration sections. /// /// The same container for chaining. public IContainer RegisterDefaultCoreConfigSections() @@ -115,7 +113,7 @@ public IContainer RegisterDefaultCoreConfigSections() } /// - /// Registers the default event bus service in the container. + /// Registers the default event bus service in the container. /// /// The same container for chaining. public IContainer RegisterEventBusService() @@ -125,18 +123,16 @@ public IContainer RegisterEventBusService() resolver => new EventBusService(resolver.Resolve()), Reuse.Singleton ); - container.AddToRegisterTypedList( - new ServiceRegistrationData(typeof(IEventBus), typeof(EventBusService), -1) - ); + container.AddToRegisterTypedList(new ServiceRegistrationData(typeof(IEventBus), typeof(EventBusService), -1)); container.RegisterStdService(-900); return container; } /// - /// Registers the recursive file watcher service as a singleton resolving the event bus. - /// Not part of : opt in, then call - /// for the directories to watch. + /// Registers the recursive file watcher service as a singleton resolving the event bus. + /// Not part of : opt in, then call + /// for the directories to watch. /// /// Optional debounce window; defaults to 300ms when null. /// The same container for chaining. @@ -144,8 +140,8 @@ public IContainer RegisterFileWatcherService(TimeSpan? debounceDelay = null) { container.RegisterDelegate( resolver => debounceDelay is { } delay - ? new FileWatcherService(resolver.Resolve(), delay) - : new FileWatcherService(resolver.Resolve()), + ? new FileWatcherService(resolver.Resolve(), delay) + : new FileWatcherService(resolver.Resolve()), Reuse.Singleton ); @@ -153,7 +149,7 @@ public IContainer RegisterFileWatcherService(TimeSpan? debounceDelay = null) } /// - /// Registers the default job system service in the container. + /// Registers the default job system service in the container. /// /// The same container for chaining. public IContainer RegisterJobSystemService() @@ -164,16 +160,14 @@ public IContainer RegisterJobSystemService() } /// - /// Registers the default main-thread dispatcher service in the container. + /// Registers the default main-thread dispatcher service in the container. /// /// The same container for chaining. public IContainer RegisterMainThreadDispatcherService() - { - return container.RegisterStdService(-1); - } + => container.RegisterStdService(-1); /// - /// Registers the default metrics collection service in the container. + /// Registers the default metrics collection service in the container. /// /// The same container for chaining. public IContainer RegisterMetricsCollectionService() @@ -184,7 +178,7 @@ public IContainer RegisterMetricsCollectionService() } /// - /// Registers default encrypted local secret services in the container. + /// Registers default encrypted local secret services in the container. /// /// The same container for chaining. public IContainer RegisterSecretServices() @@ -197,7 +191,7 @@ public IContainer RegisterSecretServices() } /// - /// Registers the default timer wheel service in the container. + /// Registers the default timer wheel service in the container. /// /// The same container for chaining. public IContainer RegisterTimerWheelService() diff --git a/src/SquidStd.Services.Core/Extensions/RegisterHealthChecksServiceExtension.cs b/src/SquidStd.Services.Core/Extensions/RegisterHealthChecksServiceExtension.cs index a15c540e..b04fd4f9 100644 --- a/src/SquidStd.Services.Core/Extensions/RegisterHealthChecksServiceExtension.cs +++ b/src/SquidStd.Services.Core/Extensions/RegisterHealthChecksServiceExtension.cs @@ -7,7 +7,7 @@ namespace SquidStd.Services.Core.Extensions; /// -/// Extension methods for registering the health-check aggregator. +/// Extension methods for registering the health-check aggregator. /// public static class RegisterHealthChecksServiceExtension { @@ -15,8 +15,8 @@ public static class RegisterHealthChecksServiceExtension extension(IContainer container) { /// - /// Registers the health-check aggregator () as a singleton. - /// Concrete checks register themselves as IHealthCheck and are collected automatically. + /// Registers the health-check aggregator () as a singleton. + /// Concrete checks register themselves as IHealthCheck and are collected automatically. /// /// The same container for chaining. public IContainer RegisterHealthChecksService() diff --git a/src/SquidStd.Services.Core/Extensions/RegisterSchedulerServicesExtension.cs b/src/SquidStd.Services.Core/Extensions/RegisterSchedulerServicesExtension.cs index 98473b2d..dbed6903 100644 --- a/src/SquidStd.Services.Core/Extensions/RegisterSchedulerServicesExtension.cs +++ b/src/SquidStd.Services.Core/Extensions/RegisterSchedulerServicesExtension.cs @@ -8,7 +8,7 @@ namespace SquidStd.Services.Core.Extensions; /// -/// Extension methods for registering the SquidStd cron scheduler. +/// Extension methods for registering the SquidStd cron scheduler. /// public static class RegisterSchedulerServicesExtension { @@ -16,8 +16,8 @@ public static class RegisterSchedulerServicesExtension extension(IContainer container) { /// - /// Registers the timer wheel pump and the cron scheduler. Must be called after - /// RegisterCoreServices so that ITimerService and IJobSystem exist. + /// Registers the timer wheel pump and the cron scheduler. Must be called after + /// RegisterCoreServices so that ITimerService and IJobSystem exist. /// /// The same container for chaining. public IContainer RegisterSchedulerServices() diff --git a/src/SquidStd.Services.Core/Services/Bootstrap/SquidStdBootstrap.cs b/src/SquidStd.Services.Core/Services/Bootstrap/SquidStdBootstrap.cs index 1d10c44d..d8ab9057 100644 --- a/src/SquidStd.Services.Core/Services/Bootstrap/SquidStdBootstrap.cs +++ b/src/SquidStd.Services.Core/Services/Bootstrap/SquidStdBootstrap.cs @@ -14,7 +14,7 @@ namespace SquidStd.Services.Core.Services.Bootstrap; /// -/// Default SquidStd bootstrapper and service lifecycle orchestrator. +/// Default SquidStd bootstrapper and service lifecycle orchestrator. /// public sealed class SquidStdBootstrap : ISquidStdBootstrap { @@ -32,21 +32,17 @@ public sealed class SquidStdBootstrap : ISquidStdBootstrap public IContainer Container { get; } /// - /// Initializes a bootstrapper with default options. + /// Initializes a bootstrapper with default options. /// public SquidStdBootstrap() - : this(new SquidStdOptions()) - { - } + : this(new()) { } /// - /// Initializes a bootstrapper with the specified options. + /// Initializes a bootstrapper with the specified options. /// /// Bootstrap options used to register core services. public SquidStdBootstrap(SquidStdOptions options) - : this(options, new Container(), true) - { - } + : this(options, new Container(), true) { } private SquidStdBootstrap(SquidStdOptions options, IContainer container, bool ownsContainer) { @@ -67,9 +63,7 @@ private SquidStdBootstrap(SquidStdOptions options, IContainer container, bool ow /// public ISquidStdBootstrap ConfigureService(Func configure) - { - return ConfigureServices(configure); - } + => ConfigureServices(configure); /// public ISquidStdBootstrap ConfigureServices(Func configure) @@ -88,8 +82,8 @@ public ISquidStdBootstrap ConfigureServices(Func configu var configuredContainer = configure(Container); return !ReferenceEquals(configuredContainer, Container) - ? throw new InvalidOperationException("ConfigureServices must return the bootstrap container instance.") - : this; + ? throw new InvalidOperationException("ConfigureServices must return the bootstrap container instance.") + : this; } /// @@ -135,9 +129,7 @@ public async Task RunAsync(CancellationToken cancellationToken = default) { await Task.Delay(Timeout.InfiniteTimeSpan, cancellationToken); } - catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) - { - } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { } finally { await StopAsync(CancellationToken.None); @@ -207,34 +199,28 @@ public async ValueTask StopAsync(CancellationToken cancellationToken = default) } /// - /// Creates a bootstrapper with default options. + /// Creates a bootstrapper with default options. /// /// The created bootstrapper. public static SquidStdBootstrap Create() - { - return new SquidStdBootstrap(); - } + => new(); /// - /// Creates a bootstrapper with the specified options. + /// Creates a bootstrapper with the specified options. /// /// Bootstrap options used to register core services. /// The created bootstrapper. public static SquidStdBootstrap Create(SquidStdOptions options) - { - return new SquidStdBootstrap(options); - } + => new(options); /// - /// Creates a bootstrapper using an externally owned DryIoc container. + /// Creates a bootstrapper using an externally owned DryIoc container. /// /// Bootstrap options used to register core services. /// Externally owned container that receives SquidStd services. /// The created bootstrapper. public static SquidStdBootstrap Create(SquidStdOptions options, IContainer container) - { - return new SquidStdBootstrap(options, container, false); - } + => new(options, container, false); private void ConfigureLogger() { @@ -282,9 +268,9 @@ private ServiceRegistrationData[] GetServiceRegistrations() return [ .. Container.Resolve>() - .OrderBy(registration => registration.Priority) - .ThenBy(registration => registration.ServiceType.FullName, StringComparer.Ordinal) - .ThenBy(registration => registration.ImplementationType.FullName, StringComparer.Ordinal) + .OrderBy(registration => registration.Priority) + .ThenBy(registration => registration.ServiceType.FullName, StringComparer.Ordinal) + .ThenBy(registration => registration.ImplementationType.FullName, StringComparer.Ordinal) ]; } @@ -341,8 +327,8 @@ private string ResolveLogPath(SquidStdLoggerOptions options) var logDirectory = string.IsNullOrWhiteSpace(options.LogDirectory) ? "logs" : options.LogDirectory; var fileName = string.IsNullOrWhiteSpace(options.FileName) ? "squidstd-.log" : options.FileName; var directory = Path.IsPathRooted(logDirectory) - ? logDirectory - : Path.Combine(Options.RootDirectory, logDirectory); + ? logDirectory + : Path.Combine(Options.RootDirectory, logDirectory); return Path.Combine(directory, fileName); } diff --git a/src/SquidStd.Services.Core/Services/CommandDispatcher.cs b/src/SquidStd.Services.Core/Services/CommandDispatcher.cs index 5db4f86d..c888f198 100644 --- a/src/SquidStd.Services.Core/Services/CommandDispatcher.cs +++ b/src/SquidStd.Services.Core/Services/CommandDispatcher.cs @@ -8,8 +8,8 @@ namespace SquidStd.Services.Core.Services; /// -/// In-process command dispatcher with parallel per-handler fan-out, fault isolation, and a dispatch -/// result. Commands route by their CLR type within the ambient . +/// In-process command dispatcher with parallel per-handler fan-out, fault isolation, and a dispatch +/// result. Commands route by their CLR type within the ambient . /// /// The ambient context type. public sealed class CommandDispatcher : ICommandDispatcher, IDisposable @@ -38,7 +38,9 @@ public IDisposable Subscribe(Func public async Task DispatchAsync( - TCommand command, TContext context, CancellationToken cancellationToken = default + TCommand command, + TContext context, + CancellationToken cancellationToken = default ) where TCommand : ICommand { @@ -48,7 +50,7 @@ public async Task DispatchAsync( if (handlers is null) { - return new CommandDispatchResult(false, 0, []); + return new(false, 0, []); } var tasks = new Task[handlers.Length]; @@ -61,7 +63,7 @@ public async Task DispatchAsync( var outcomes = await Task.WhenAll(tasks); var errors = outcomes.Where(static error => error is not null).Select(static error => error!).ToArray(); - return new CommandDispatchResult(true, handlers.Length, errors); + return new(true, handlers.Length, errors); } private CommandSubscription Add(Type commandType, object handler) @@ -75,7 +77,7 @@ private CommandSubscription Add(Type commandType, object handler) bucket.Add(handler); } - return new CommandSubscription(bucket, handler); + return new(bucket, handler); } private object[]? Snapshot(Type commandType) @@ -92,7 +94,10 @@ private CommandSubscription Add(Type commandType, object handler) } private async Task DispatchSafeAsync( - object handler, TCommand command, TContext context, CancellationToken cancellationToken + object handler, + TCommand command, + TContext context, + CancellationToken cancellationToken ) where TCommand : ICommand { @@ -118,7 +123,7 @@ private CommandSubscription Add(Type commandType, object handler) typeof(TCommand).Name ); - return new CommandHandlerError(handler.GetType(), ex); + return new(handler.GetType(), ex); } } diff --git a/src/SquidStd.Services.Core/Services/CommandDispatcherActivator.cs b/src/SquidStd.Services.Core/Services/CommandDispatcherActivator.cs index 832fdcea..8e177eaa 100644 --- a/src/SquidStd.Services.Core/Services/CommandDispatcherActivator.cs +++ b/src/SquidStd.Services.Core/Services/CommandDispatcherActivator.cs @@ -6,8 +6,8 @@ namespace SquidStd.Services.Core.Services; /// -/// Subscribes every DI-registered command handler to the dispatcher at startup, before publishing -/// services run. +/// Subscribes every DI-registered command handler to the dispatcher at startup, before publishing +/// services run. /// /// The dispatcher context type. internal sealed class CommandDispatcherActivator : ISquidStdService @@ -37,7 +37,5 @@ public ValueTask StartAsync(CancellationToken cancellationToken = default) } public ValueTask StopAsync(CancellationToken cancellationToken = default) - { - return ValueTask.CompletedTask; - } + => ValueTask.CompletedTask; } diff --git a/src/SquidStd.Services.Core/Services/ConfigManagerService.cs b/src/SquidStd.Services.Core/Services/ConfigManagerService.cs index e590af58..09ac24d7 100644 --- a/src/SquidStd.Services.Core/Services/ConfigManagerService.cs +++ b/src/SquidStd.Services.Core/Services/ConfigManagerService.cs @@ -9,7 +9,7 @@ namespace SquidStd.Services.Core.Services; /// -/// Loads YAML configuration sections and registers them into DryIoc. +/// Loads YAML configuration sections and registers them into DryIoc. /// public sealed class ConfigManagerService : IConfigManagerService, ISquidStdService { @@ -30,7 +30,7 @@ public sealed class ConfigManagerService : IConfigManagerService, ISquidStdServi public IReadOnlyCollection Entries => GetEntries(); /// - /// Initializes the config manager service. + /// Initializes the config manager service. /// /// Container that receives loaded configuration sections. /// Logical configuration name or YAML file name. @@ -48,15 +48,11 @@ public ConfigManagerService(IContainer container, string configName, string conf /// public string Compose() - { - return YamlUtils.SerializeSections(BuildSectionMap()); - } + => YamlUtils.SerializeSections(BuildSectionMap()); /// public TConfig GetConfig() where TConfig : class - { - return _container.Resolve(); - } + => _container.Resolve(); /// public void Load() @@ -70,11 +66,11 @@ public void Load() { var entry = entries[i]; var value = string.IsNullOrWhiteSpace(yaml) - ? entry.CreateDefault() - : YamlUtils.DeserializeSection(yaml, entry.SectionName, entry.ConfigType) ?? - entry.CreateDefault(); + ? entry.CreateDefault() + : YamlUtils.DeserializeSection(yaml, entry.SectionName, entry.ConfigType) ?? + entry.CreateDefault(); - ApplyEnvSubstitution(value, new HashSet(ReferenceEqualityComparer.Instance)); + ApplyEnvSubstitution(value, new(ReferenceEqualityComparer.Instance)); _values[entry.ConfigType] = value; _container.RegisterInstance(entry.ConfigType, value, IfAlreadyRegistered.Replace); @@ -88,9 +84,7 @@ public void Load() /// public void Save() - { - YamlUtils.SerializeToFile(BuildSectionMap(), ConfigPath); - } + => YamlUtils.SerializeToFile(BuildSectionMap(), ConfigPath); /// public ValueTask StartAsync(CancellationToken cancellationToken = default) @@ -176,9 +170,7 @@ private Dictionary BuildSectionMap() } private IReadOnlyCollection GetEntries() - { - return GetRegistrations().Cast().ToArray(); - } + => GetRegistrations().Cast().ToArray(); private List GetRegistrations() { @@ -190,8 +182,8 @@ private List GetRegistrations() return [ .. _container.Resolve>() - .OrderBy(entry => entry.Priority) - .ThenBy(entry => entry.SectionName, StringComparer.Ordinal) + .OrderBy(entry => entry.Priority) + .ThenBy(entry => entry.SectionName, StringComparer.Ordinal) ]; } diff --git a/src/SquidStd.Services.Core/Services/EventBusService.cs b/src/SquidStd.Services.Core/Services/EventBusService.cs index e570a3b5..2b1ea54b 100644 --- a/src/SquidStd.Services.Core/Services/EventBusService.cs +++ b/src/SquidStd.Services.Core/Services/EventBusService.cs @@ -8,8 +8,8 @@ namespace SquidStd.Services.Core.Services; /// -/// In-process event bus with parallel per-listener dispatch, catch-all listeners, -/// fault isolation, and slow-listener telemetry. +/// In-process event bus with parallel per-listener dispatch, catch-all listeners, +/// fault isolation, and slow-listener telemetry. /// public sealed class EventBusService : IEventBus, IDisposable { @@ -19,15 +19,13 @@ public sealed class EventBusService : IEventBus, IDisposable private bool _disposed; /// - /// Initializes the event bus with default options. + /// Initializes the event bus with default options. /// public EventBusService() - : this(new EventBusOptions()) - { - } + : this(new()) { } /// - /// Initializes the event bus with the supplied options. + /// Initializes the event bus with the supplied options. /// public EventBusService(EventBusOptions options) { @@ -37,9 +35,7 @@ public EventBusService(EventBusOptions options) /// public void Publish(TEvent eventData) where TEvent : IEvent - { - PublishAsync(eventData, CancellationToken.None).GetAwaiter().GetResult(); - } + => PublishAsync(eventData, CancellationToken.None).GetAwaiter().GetResult(); /// public async Task PublishAsync(TEvent eventData, CancellationToken cancellationToken = default) @@ -116,7 +112,7 @@ private Subscription Add(Type eventType, object listener) bucket.Add(listener); } - return new Subscription(bucket, listener); + return new(bucket, listener); } private object[]? Snapshot(Type eventType) diff --git a/src/SquidStd.Services.Core/Services/EventListenerActivator.cs b/src/SquidStd.Services.Core/Services/EventListenerActivator.cs index 35517a35..ce487ec5 100644 --- a/src/SquidStd.Services.Core/Services/EventListenerActivator.cs +++ b/src/SquidStd.Services.Core/Services/EventListenerActivator.cs @@ -6,7 +6,7 @@ namespace SquidStd.Services.Core.Services; /// -/// Subscribes every DI-registered event listener to the bus at startup, before publishing services run. +/// Subscribes every DI-registered event listener to the bus at startup, before publishing services run. /// internal sealed class EventListenerActivator : ISquidStdService { @@ -35,7 +35,5 @@ public ValueTask StartAsync(CancellationToken cancellationToken = default) } public ValueTask StopAsync(CancellationToken cancellationToken = default) - { - return ValueTask.CompletedTask; - } + => ValueTask.CompletedTask; } diff --git a/src/SquidStd.Services.Core/Services/HealthCheckService.cs b/src/SquidStd.Services.Core/Services/HealthCheckService.cs index 76377539..634a119b 100644 --- a/src/SquidStd.Services.Core/Services/HealthCheckService.cs +++ b/src/SquidStd.Services.Core/Services/HealthCheckService.cs @@ -8,8 +8,8 @@ namespace SquidStd.Services.Core.Services; /// -/// Runs every registered in parallel with a per-check timeout and -/// exception isolation, then aggregates the results into a single . +/// Runs every registered in parallel with a per-check timeout and +/// exception isolation, then aggregates the results into a single . /// public sealed class HealthCheckService : IHealthCheckService { @@ -36,7 +36,7 @@ public async ValueTask CheckHealthAsync(CancellationToken cancella if (_checks.Length == 0) { - return new HealthReport + return new() { Status = HealthStatus.Healthy, Entries = new Dictionary(StringComparer.Ordinal), @@ -77,7 +77,7 @@ public async ValueTask CheckHealthAsync(CancellationToken cancella } } - return new HealthReport + return new() { Status = overall, Entries = entries, @@ -105,7 +105,7 @@ CancellationToken cancellationToken catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested) { return (check.Name, - HealthCheckResult.Unhealthy($"Timed out after {_checkTimeout}.") with { Duration = stopwatch.Elapsed }); + HealthCheckResult.Unhealthy($"Timed out after {_checkTimeout}.") with { Duration = stopwatch.Elapsed }); } catch (OperationCanceledException) { diff --git a/src/SquidStd.Services.Core/Services/Internal/CommandSubscription.cs b/src/SquidStd.Services.Core/Services/Internal/CommandSubscription.cs index 97bb29c5..39f441c8 100644 --- a/src/SquidStd.Services.Core/Services/Internal/CommandSubscription.cs +++ b/src/SquidStd.Services.Core/Services/Internal/CommandSubscription.cs @@ -1,7 +1,7 @@ namespace SquidStd.Services.Core.Services.Internal; /// -/// Unsubscribe token that removes a command handler from its bucket when disposed. +/// Unsubscribe token that removes a command handler from its bucket when disposed. /// internal sealed class CommandSubscription : IDisposable { diff --git a/src/SquidStd.Services.Core/Services/Internal/CronJobEntry.cs b/src/SquidStd.Services.Core/Services/Internal/CronJobEntry.cs index 705f2f77..617ea42e 100644 --- a/src/SquidStd.Services.Core/Services/Internal/CronJobEntry.cs +++ b/src/SquidStd.Services.Core/Services/Internal/CronJobEntry.cs @@ -3,7 +3,7 @@ namespace SquidStd.Services.Core.Services.Internal; /// -/// Internal mutable state for a registered cron job. +/// Internal mutable state for a registered cron job. /// internal sealed class CronJobEntry { diff --git a/src/SquidStd.Services.Core/Services/Internal/DelegateCommandHandler.cs b/src/SquidStd.Services.Core/Services/Internal/DelegateCommandHandler.cs index 957f0db7..fe4674d9 100644 --- a/src/SquidStd.Services.Core/Services/Internal/DelegateCommandHandler.cs +++ b/src/SquidStd.Services.Core/Services/Internal/DelegateCommandHandler.cs @@ -3,8 +3,8 @@ namespace SquidStd.Services.Core.Services.Internal; /// -/// Adapts a delegate to so the dispatcher has a -/// single dispatch path. +/// Adapts a delegate to so the dispatcher has a +/// single dispatch path. /// /// The command type. /// The context type. @@ -19,7 +19,5 @@ public DelegateCommandHandler(Func } public Task HandleAsync(TCommand command, TContext context, CancellationToken cancellationToken = default) - { - return _handler(command, context, cancellationToken); - } + => _handler(command, context, cancellationToken); } diff --git a/src/SquidStd.Services.Core/Services/Internal/DelegateEventListener.cs b/src/SquidStd.Services.Core/Services/Internal/DelegateEventListener.cs index 3e64fa7b..7f966981 100644 --- a/src/SquidStd.Services.Core/Services/Internal/DelegateEventListener.cs +++ b/src/SquidStd.Services.Core/Services/Internal/DelegateEventListener.cs @@ -3,7 +3,7 @@ namespace SquidStd.Services.Core.Services.Internal; /// -/// Adapts a delegate handler to so the bus has a single dispatch path. +/// Adapts a delegate handler to so the bus has a single dispatch path. /// /// The event type. internal sealed class DelegateEventListener : IEventListener @@ -17,7 +17,5 @@ public DelegateEventListener(Func handler) } public Task HandleAsync(TEvent eventData, CancellationToken cancellationToken = default) - { - return _handler(eventData, cancellationToken); - } + => _handler(eventData, cancellationToken); } diff --git a/src/SquidStd.Services.Core/Services/Internal/JobItem.cs b/src/SquidStd.Services.Core/Services/Internal/JobItem.cs index 3e9f3014..71fd52ae 100644 --- a/src/SquidStd.Services.Core/Services/Internal/JobItem.cs +++ b/src/SquidStd.Services.Core/Services/Internal/JobItem.cs @@ -1,7 +1,7 @@ namespace SquidStd.Services.Core.Services.Internal; /// -/// Internal envelope for a scheduled job. +/// Internal envelope for a scheduled job. /// internal sealed class JobItem { @@ -17,12 +17,8 @@ public JobItem(Action run, Action cancel) } public void Cancel() - { - _cancel(); - } + => _cancel(); public void Run() - { - _run(); - } + => _run(); } diff --git a/src/SquidStd.Services.Core/Services/Internal/MainThreadSynchronizationContext.cs b/src/SquidStd.Services.Core/Services/Internal/MainThreadSynchronizationContext.cs index 01630bf6..65255cbf 100644 --- a/src/SquidStd.Services.Core/Services/Internal/MainThreadSynchronizationContext.cs +++ b/src/SquidStd.Services.Core/Services/Internal/MainThreadSynchronizationContext.cs @@ -3,7 +3,7 @@ namespace SquidStd.Services.Core.Services.Internal; /// -/// Synchronization context that forwards asynchronous posts to a main-thread dispatcher. +/// Synchronization context that forwards asynchronous posts to a main-thread dispatcher. /// internal sealed class MainThreadSynchronizationContext : SynchronizationContext { diff --git a/src/SquidStd.Services.Core/Services/Internal/Subscription.cs b/src/SquidStd.Services.Core/Services/Internal/Subscription.cs index 6264dcfd..a0753960 100644 --- a/src/SquidStd.Services.Core/Services/Internal/Subscription.cs +++ b/src/SquidStd.Services.Core/Services/Internal/Subscription.cs @@ -1,7 +1,7 @@ namespace SquidStd.Services.Core.Services.Internal; /// -/// Unsubscribe token that removes a listener from its bucket when disposed. +/// Unsubscribe token that removes a listener from its bucket when disposed. /// internal sealed class Subscription : IDisposable { diff --git a/src/SquidStd.Services.Core/Services/Internal/TimerEntry.cs b/src/SquidStd.Services.Core/Services/Internal/TimerEntry.cs index 88a5415a..d13435d3 100644 --- a/src/SquidStd.Services.Core/Services/Internal/TimerEntry.cs +++ b/src/SquidStd.Services.Core/Services/Internal/TimerEntry.cs @@ -1,7 +1,7 @@ namespace SquidStd.Services.Core.Services.Internal; /// -/// Internal mutable timer wheel entry. +/// Internal mutable timer wheel entry. /// internal sealed class TimerEntry { diff --git a/src/SquidStd.Services.Core/Services/JobSystemService.cs b/src/SquidStd.Services.Core/Services/JobSystemService.cs index b36ed572..07ae39d1 100644 --- a/src/SquidStd.Services.Core/Services/JobSystemService.cs +++ b/src/SquidStd.Services.Core/Services/JobSystemService.cs @@ -8,7 +8,7 @@ namespace SquidStd.Services.Core.Services; /// -/// Schedules jobs on a fixed set of worker threads. +/// Schedules jobs on a fixed set of worker threads. /// public sealed class JobSystemService : IJobSystem, ISquidStdService { @@ -36,7 +36,7 @@ public sealed class JobSystemService : IJobSystem, ISquidStdService public int WorkerCount { get; } /// - /// Initializes the job system service. + /// Initializes the job system service. /// /// Job system configuration. public JobSystemService(JobsConfig config) @@ -45,7 +45,7 @@ public JobSystemService(JobsConfig config) _config = config; WorkerCount = ResolveWorkerCount(config.WorkerThreadCount); _channel = Channel.CreateUnbounded( - new UnboundedChannelOptions + new() { SingleReader = false, SingleWriter = false @@ -200,9 +200,7 @@ private void OnUnobservedTaskException(object? sender, UnobservedTaskExceptionEv } private static int ResolveWorkerCount(int configured) - { - return configured > 0 ? configured : Math.Max(1, Environment.ProcessorCount - 1); - } + => configured > 0 ? configured : Math.Max(1, Environment.ProcessorCount - 1); private void Stop(CancellationToken cancellationToken) { @@ -315,10 +313,8 @@ private void WorkerLoop() } /// - /// Releases worker resources. + /// Releases worker resources. /// public void Dispose() - { - Stop(CancellationToken.None); - } + => Stop(CancellationToken.None); } diff --git a/src/SquidStd.Services.Core/Services/MainThreadDispatcherService.cs b/src/SquidStd.Services.Core/Services/MainThreadDispatcherService.cs index a6c84e6a..bbabe5e4 100644 --- a/src/SquidStd.Services.Core/Services/MainThreadDispatcherService.cs +++ b/src/SquidStd.Services.Core/Services/MainThreadDispatcherService.cs @@ -6,7 +6,7 @@ namespace SquidStd.Services.Core.Services; /// -/// Queues callbacks and drains them on the calling thread. +/// Queues callbacks and drains them on the calling thread. /// public sealed class MainThreadDispatcherService : IMainThreadDispatcher { diff --git a/src/SquidStd.Services.Core/Services/MetricsCollectionService.cs b/src/SquidStd.Services.Core/Services/MetricsCollectionService.cs index c34322fb..3d346482 100644 --- a/src/SquidStd.Services.Core/Services/MetricsCollectionService.cs +++ b/src/SquidStd.Services.Core/Services/MetricsCollectionService.cs @@ -9,7 +9,7 @@ namespace SquidStd.Services.Core.Services; /// -/// Periodically collects metrics from registered providers and stores the latest snapshot. +/// Periodically collects metrics from registered providers and stores the latest snapshot. /// public sealed class MetricsCollectionService : IMetricsCollectionService, ISquidStdService, IDisposable { @@ -31,7 +31,7 @@ public sealed class MetricsCollectionService : IMetricsCollectionService, ISquid private int _started; /// - /// Initializes the metrics collection service. + /// Initializes the metrics collection service. /// /// Metric providers to collect from. /// Metrics collection configuration. @@ -66,9 +66,7 @@ public MetricsSnapshot GetSnapshot() /// public MetricsSnapshot GetStatus() - { - return GetSnapshot(); - } + => GetSnapshot(); /// public ValueTask StartAsync(CancellationToken cancellationToken = default) @@ -89,7 +87,7 @@ public ValueTask StartAsync(CancellationToken cancellationToken = default) if (_lifetimeCts.IsCancellationRequested) { _lifetimeCts.Dispose(); - _lifetimeCts = new CancellationTokenSource(); + _lifetimeCts = new(); } _collectionTask = Task.Run(() => RunCollectionLoopAsync(_lifetimeCts.Token), _lifetimeCts.Token); @@ -113,9 +111,7 @@ public async ValueTask StopAsync(CancellationToken cancellationToken = default) { await _collectionTask; } - catch (OperationCanceledException) - { - } + catch (OperationCanceledException) { } } private async Task CollectOnceAsync(CancellationToken cancellationToken) @@ -161,10 +157,8 @@ private async Task CollectOnceAsync(CancellationToken cancellationToken) } private static string CreateMetricKey(string providerName, string metricName) - { - return string.IsNullOrWhiteSpace(providerName) ? metricName : - string.IsNullOrWhiteSpace(metricName) ? providerName : providerName + "." + metricName; - } + => string.IsNullOrWhiteSpace(providerName) ? metricName : + string.IsNullOrWhiteSpace(metricName) ? providerName : providerName + "." + metricName; private void LogProviderCollection(IMetricProvider provider, int metricCount) { @@ -225,7 +219,7 @@ private void ThrowIfDisposed() } /// - /// Releases metrics collection resources. + /// Releases metrics collection resources. /// public void Dispose() { @@ -242,9 +236,7 @@ public void Dispose() { _collectionTask.GetAwaiter().GetResult(); } - catch (OperationCanceledException) - { - } + catch (OperationCanceledException) { } } _lifetimeCts.Dispose(); diff --git a/src/SquidStd.Services.Core/Services/Scheduling/CronSchedulerService.cs b/src/SquidStd.Services.Core/Services/Scheduling/CronSchedulerService.cs index a4fe9da9..8045b477 100644 --- a/src/SquidStd.Services.Core/Services/Scheduling/CronSchedulerService.cs +++ b/src/SquidStd.Services.Core/Services/Scheduling/CronSchedulerService.cs @@ -11,9 +11,9 @@ namespace SquidStd.Services.Core.Services.Scheduling; /// -/// Cron scheduler built on the timer wheel: each job is a one-shot, self-rescheduling -/// timer. On fire, the handler is dispatched through ; an occurrence -/// is skipped when the previous run of the same job is still in flight. +/// Cron scheduler built on the timer wheel: each job is a one-shot, self-rescheduling +/// timer. On fire, the handler is dispatched through ; an occurrence +/// is skipped when the previous run of the same job is still in flight. /// public sealed class CronSchedulerService : ICronScheduler, ISquidStdService, IDisposable { @@ -27,18 +27,19 @@ public sealed class CronSchedulerService : ICronScheduler, ISquidStdService, IDi /// public IReadOnlyCollection Jobs => _entries.Values - .Select(entry => new CronJobInfo - { - JobId = entry.JobId, - Name = entry.Name, - CronExpression = entry.CronText, - NextOccurrenceUtc = entry.NextOccurrenceUtc, - IsRunning = Volatile.Read(ref entry.Running) == 1, - LastRunUtc = entry.LastRunUtc, - RunCount = Interlocked.Read(ref entry.RunCount) - } - ) - .ToArray(); + .Select( + entry => new CronJobInfo + { + JobId = entry.JobId, + Name = entry.Name, + CronExpression = entry.CronText, + NextOccurrenceUtc = entry.NextOccurrenceUtc, + IsRunning = Volatile.Read(ref entry.Running) == 1, + LastRunUtc = entry.LastRunUtc, + RunCount = Interlocked.Read(ref entry.RunCount) + } + ) + .ToArray(); public CronSchedulerService(ITimerService timer, IJobSystem jobs) { @@ -105,9 +106,7 @@ public int UnscheduleByName(string name) /// public ValueTask StartAsync(CancellationToken cancellationToken = default) - { - return ValueTask.CompletedTask; - } + => ValueTask.CompletedTask; /// public ValueTask StopAsync(CancellationToken cancellationToken = default) diff --git a/src/SquidStd.Services.Core/Services/Scheduling/TimerWheelPumpService.cs b/src/SquidStd.Services.Core/Services/Scheduling/TimerWheelPumpService.cs index ed849d08..c0f07ff1 100644 --- a/src/SquidStd.Services.Core/Services/Scheduling/TimerWheelPumpService.cs +++ b/src/SquidStd.Services.Core/Services/Scheduling/TimerWheelPumpService.cs @@ -7,9 +7,9 @@ namespace SquidStd.Services.Core.Services.Scheduling; /// -/// Periodically advances the timer wheel so that wheel-backed timers fire in a normal -/// (non-game-loop) application. Drives on a -/// background loop. +/// Periodically advances the timer wheel so that wheel-backed timers fire in a normal +/// (non-game-loop) application. Drives on a +/// background loop. /// public sealed class TimerWheelPumpService : ISquidStdService, IDisposable { diff --git a/src/SquidStd.Services.Core/Services/SeededCommandDispatcher.cs b/src/SquidStd.Services.Core/Services/SeededCommandDispatcher.cs index a9c4fd13..bc2a0161 100644 --- a/src/SquidStd.Services.Core/Services/SeededCommandDispatcher.cs +++ b/src/SquidStd.Services.Core/Services/SeededCommandDispatcher.cs @@ -4,9 +4,9 @@ namespace SquidStd.Services.Core.Services; /// -/// Seeded dispatcher that builds the context from a seed via an -/// and forwards to the underlying -/// . +/// Seeded dispatcher that builds the context from a seed via an +/// and forwards to the underlying +/// . /// /// The ambient context type. /// The seed the context is built from. @@ -26,10 +26,10 @@ ICommandContextFactory contextFactory /// public Task DispatchAsync( - TCommand command, TSeed seed, CancellationToken cancellationToken = default + TCommand command, + TSeed seed, + CancellationToken cancellationToken = default ) where TCommand : ICommand - { - return _dispatcher.DispatchAsync(command, _contextFactory.Create(seed), cancellationToken); - } + => _dispatcher.DispatchAsync(command, _contextFactory.Create(seed), cancellationToken); } diff --git a/src/SquidStd.Services.Core/Services/Storage/AesGcmSecretProtector.cs b/src/SquidStd.Services.Core/Services/Storage/AesGcmSecretProtector.cs index 0e740776..d1230cbf 100644 --- a/src/SquidStd.Services.Core/Services/Storage/AesGcmSecretProtector.cs +++ b/src/SquidStd.Services.Core/Services/Storage/AesGcmSecretProtector.cs @@ -7,7 +7,7 @@ namespace SquidStd.Services.Core.Services.Storage; /// -/// Protects secrets using AES-GCM and a key supplied by environment variable. +/// Protects secrets using AES-GCM and a key supplied by environment variable. /// public sealed class AesGcmSecretProtector : ISecretProtector { @@ -18,7 +18,7 @@ public sealed class AesGcmSecretProtector : ISecretProtector private readonly byte[] _key; /// - /// Initializes the AES-GCM secret protector. + /// Initializes the AES-GCM secret protector. /// /// Secret storage configuration. public AesGcmSecretProtector(SecretsConfig config) @@ -75,9 +75,7 @@ public byte[] Unprotect(byte[] protectedData) } private static byte[] CreateDefaultKey() - { - return SHA256.HashData(Encoding.UTF8.GetBytes(DefaultKeyMaterial)); - } + => SHA256.HashData(Encoding.UTF8.GetBytes(DefaultKeyMaterial)); private static byte[] ResolveKey(string environmentVariable) { @@ -107,7 +105,7 @@ private static byte[] ResolveKey(string environmentVariable) } return key.Length is 16 or 24 or 32 - ? key - : throw new InvalidOperationException("Secret key must be 16, 24, or 32 bytes after base64 decoding."); + ? key + : throw new InvalidOperationException("Secret key must be 16, 24, or 32 bytes after base64 decoding."); } } diff --git a/src/SquidStd.Services.Core/Services/Storage/FileSecretStore.cs b/src/SquidStd.Services.Core/Services/Storage/FileSecretStore.cs index e497ea51..c8b763d1 100644 --- a/src/SquidStd.Services.Core/Services/Storage/FileSecretStore.cs +++ b/src/SquidStd.Services.Core/Services/Storage/FileSecretStore.cs @@ -2,14 +2,13 @@ using System.Text; using SquidStd.Core.Data.Storage; using SquidStd.Core.Interfaces.Secrets; -using SquidStd.Storage.Abstractions.Data.Config; using SquidStd.Storage.Abstractions.Interfaces; using SquidStd.Storage.Services; namespace SquidStd.Services.Core.Services.Storage; /// -/// File-backed encrypted secret store. +/// File-backed encrypted secret store. /// public sealed class FileSecretStore : ISecretStore { @@ -17,7 +16,7 @@ public sealed class FileSecretStore : ISecretStore private readonly IStorageService _storageService; /// - /// Initializes the encrypted file secret store. + /// Initializes the encrypted file secret store. /// /// Secret storage configuration. /// Secret protector used for encryption. @@ -27,20 +26,16 @@ public FileSecretStore(SecretsConfig config, ISecretProtector secretProtector) ArgumentNullException.ThrowIfNull(secretProtector); _secretProtector = secretProtector; - _storageService = new FileStorageService(new StorageConfig { RootDirectory = config.RootDirectory }); + _storageService = new FileStorageService(new() { RootDirectory = config.RootDirectory }); } /// public ValueTask DeleteAsync(string name, CancellationToken cancellationToken = default) - { - return _storageService.DeleteAsync(ToStorageKey(name), cancellationToken); - } + => _storageService.DeleteAsync(ToStorageKey(name), cancellationToken); /// public ValueTask ExistsAsync(string name, CancellationToken cancellationToken = default) - { - return _storageService.ExistsAsync(ToStorageKey(name), cancellationToken); - } + => _storageService.ExistsAsync(ToStorageKey(name), cancellationToken); /// public async ValueTask GetAsync(string name, CancellationToken cancellationToken = default) @@ -70,7 +65,8 @@ public async ValueTask SetAsync(string name, string value, CancellationToken can /// public async IAsyncEnumerable ListNamesAsync( - string? prefix = null, [EnumeratorCancellation] CancellationToken cancellationToken = default + string? prefix = null, + [EnumeratorCancellation] CancellationToken cancellationToken = default ) { const string suffix = ".secret"; diff --git a/src/SquidStd.Services.Core/Services/TimerWheelService.cs b/src/SquidStd.Services.Core/Services/TimerWheelService.cs index c26f2bcf..76703d65 100644 --- a/src/SquidStd.Services.Core/Services/TimerWheelService.cs +++ b/src/SquidStd.Services.Core/Services/TimerWheelService.cs @@ -8,7 +8,7 @@ namespace SquidStd.Services.Core.Services; /// -/// Hashed timer wheel driven by absolute timestamp updates. +/// Hashed timer wheel driven by absolute timestamp updates. /// public sealed class TimerWheelService : ITimerService, ISquidStdService { @@ -24,7 +24,7 @@ public sealed class TimerWheelService : ITimerService, ISquidStdService private long _lastTimestampMilliseconds = -1; /// - /// Initializes the timer wheel service. + /// Initializes the timer wheel service. /// /// Timer wheel configuration. public TimerWheelService(TimerWheelConfig config) @@ -53,15 +53,13 @@ public TimerWheelService(TimerWheelConfig config) for (var i = 0; i < _wheel.Length; i++) { - _wheel[i] = new LinkedList(); + _wheel[i] = new(); } } /// public ValueTask StartAsync(CancellationToken cancellationToken = default) - { - return ValueTask.CompletedTask; - } + => ValueTask.CompletedTask; /// public ValueTask StopAsync(CancellationToken cancellationToken = default) diff --git a/src/SquidStd.Services.Core/Types/BootstrapStateType.cs b/src/SquidStd.Services.Core/Types/BootstrapStateType.cs index a6a59707..2b1d72ff 100644 --- a/src/SquidStd.Services.Core/Types/BootstrapStateType.cs +++ b/src/SquidStd.Services.Core/Types/BootstrapStateType.cs @@ -1,7 +1,7 @@ namespace SquidStd.Services.Core.Types; /// -/// Lifecycle state of the SquidStd bootstrapper. +/// Lifecycle state of the SquidStd bootstrapper. /// internal enum BootstrapStateType { diff --git a/src/SquidStd.Storage.Abstractions/Data/Config/StorageConfig.cs b/src/SquidStd.Storage.Abstractions/Data/Config/StorageConfig.cs index 2fda7e43..d75be728 100644 --- a/src/SquidStd.Storage.Abstractions/Data/Config/StorageConfig.cs +++ b/src/SquidStd.Storage.Abstractions/Data/Config/StorageConfig.cs @@ -3,12 +3,12 @@ namespace SquidStd.Storage.Abstractions.Data.Config; /// -/// Configuration for local file storage. +/// Configuration for local file storage. /// public sealed class StorageConfig : IConfigEntry { /// - /// Gets or sets the root directory used by local storage. + /// Gets or sets the root directory used by local storage. /// public string RootDirectory { get; set; } = "storage"; @@ -17,7 +17,5 @@ public sealed class StorageConfig : IConfigEntry Type IConfigEntry.ConfigType => typeof(StorageConfig); object IConfigEntry.CreateDefault() - { - return new StorageConfig(); - } + => new StorageConfig(); } diff --git a/src/SquidStd.Storage.Abstractions/Interfaces/IObjectStorageService.cs b/src/SquidStd.Storage.Abstractions/Interfaces/IObjectStorageService.cs index b2e4a504..580e98d0 100644 --- a/src/SquidStd.Storage.Abstractions/Interfaces/IObjectStorageService.cs +++ b/src/SquidStd.Storage.Abstractions/Interfaces/IObjectStorageService.cs @@ -1,12 +1,12 @@ namespace SquidStd.Storage.Abstractions.Interfaces; /// -/// Stores typed objects by logical key. +/// Stores typed objects by logical key. /// public interface IObjectStorageService { /// - /// Deletes a stored object. + /// Deletes a stored object. /// /// The logical storage key. /// Token used to cancel the operation. @@ -14,7 +14,7 @@ public interface IObjectStorageService ValueTask DeleteAsync(string key, CancellationToken cancellationToken = default); /// - /// Checks whether an object exists. + /// Checks whether an object exists. /// /// The logical storage key. /// Token used to cancel the operation. @@ -22,7 +22,7 @@ public interface IObjectStorageService ValueTask ExistsAsync(string key, CancellationToken cancellationToken = default); /// - /// Enumerates stored keys, optionally filtered by prefix. + /// Enumerates stored keys, optionally filtered by prefix. /// /// Optional key prefix; null or empty returns all keys. /// Token used to cancel the enumeration. @@ -30,7 +30,7 @@ public interface IObjectStorageService IAsyncEnumerable ListKeysAsync(string? prefix = null, CancellationToken cancellationToken = default); /// - /// Loads a stored object. + /// Loads a stored object. /// /// The logical storage key. /// Token used to cancel the operation. @@ -39,7 +39,7 @@ public interface IObjectStorageService ValueTask LoadAsync(string key, CancellationToken cancellationToken = default); /// - /// Saves an object. + /// Saves an object. /// /// The logical storage key. /// The object value. diff --git a/src/SquidStd.Storage.Abstractions/Interfaces/IStorageService.cs b/src/SquidStd.Storage.Abstractions/Interfaces/IStorageService.cs index 6b469e5d..b307f2aa 100644 --- a/src/SquidStd.Storage.Abstractions/Interfaces/IStorageService.cs +++ b/src/SquidStd.Storage.Abstractions/Interfaces/IStorageService.cs @@ -1,12 +1,12 @@ namespace SquidStd.Storage.Abstractions.Interfaces; /// -/// Stores binary payloads by logical key. +/// Stores binary payloads by logical key. /// public interface IStorageService { /// - /// Deletes a stored payload. + /// Deletes a stored payload. /// /// The logical storage key. /// Token used to cancel the operation. @@ -14,7 +14,7 @@ public interface IStorageService ValueTask DeleteAsync(string key, CancellationToken cancellationToken = default); /// - /// Checks whether a payload exists. + /// Checks whether a payload exists. /// /// The logical storage key. /// Token used to cancel the operation. @@ -22,7 +22,7 @@ public interface IStorageService ValueTask ExistsAsync(string key, CancellationToken cancellationToken = default); /// - /// Enumerates stored keys, optionally filtered by prefix. + /// Enumerates stored keys, optionally filtered by prefix. /// /// Optional key prefix; null or empty returns all keys. /// Token used to cancel the enumeration. @@ -30,7 +30,7 @@ public interface IStorageService IAsyncEnumerable ListKeysAsync(string? prefix = null, CancellationToken cancellationToken = default); /// - /// Loads a binary payload. + /// Loads a binary payload. /// /// The logical storage key. /// Token used to cancel the operation. @@ -38,7 +38,7 @@ public interface IStorageService ValueTask LoadAsync(string key, CancellationToken cancellationToken = default); /// - /// Saves a binary payload atomically. + /// Saves a binary payload atomically. /// /// The logical storage key. /// The payload to store. diff --git a/src/SquidStd.Storage.Abstractions/README.md b/src/SquidStd.Storage.Abstractions/README.md index 24268d7f..fbd6e858 100644 --- a/src/SquidStd.Storage.Abstractions/README.md +++ b/src/SquidStd.Storage.Abstractions/README.md @@ -27,11 +27,11 @@ public async Task DumpKeysAsync(IStorageService storage) ## Key types -| Type | Purpose | -|------|---------| -| `IStorageService` | Binary blob store: `SaveAsync` / `LoadAsync` / `DeleteAsync` / `ExistsAsync` / `ListKeysAsync`. | -| `IObjectStorageService` | Typed object store over a blob backend (serialized by the provider). | -| `StorageConfig` | Root directory for file storage. | +| Type | Purpose | +|-------------------------|-------------------------------------------------------------------------------------------------| +| `IStorageService` | Binary blob store: `SaveAsync` / `LoadAsync` / `DeleteAsync` / `ExistsAsync` / `ListKeysAsync`. | +| `IObjectStorageService` | Typed object store over a blob backend (serialized by the provider). | +| `StorageConfig` | Root directory for file storage. | ## Related diff --git a/src/SquidStd.Storage.S3/Data/Config/S3StorageOptions.cs b/src/SquidStd.Storage.S3/Data/Config/S3StorageOptions.cs index c26069bc..071ae4d3 100644 --- a/src/SquidStd.Storage.S3/Data/Config/S3StorageOptions.cs +++ b/src/SquidStd.Storage.S3/Data/Config/S3StorageOptions.cs @@ -3,8 +3,8 @@ namespace SquidStd.Storage.S3.Data.Config; /// -/// Connection options for the S3-compatible (MinIO) storage provider. Connection details live in -/// (shared with other AWS-SDK providers); is storage-specific. +/// Connection options for the S3-compatible (MinIO) storage provider. Connection details live in +/// (shared with other AWS-SDK providers); is storage-specific. /// public sealed class S3StorageOptions { diff --git a/src/SquidStd.Storage.S3/Extensions/S3StorageRegistrationExtensions.cs b/src/SquidStd.Storage.S3/Extensions/S3StorageRegistrationExtensions.cs index 0fe1eff8..14fc320a 100644 --- a/src/SquidStd.Storage.S3/Extensions/S3StorageRegistrationExtensions.cs +++ b/src/SquidStd.Storage.S3/Extensions/S3StorageRegistrationExtensions.cs @@ -6,7 +6,7 @@ namespace SquidStd.Storage.S3.Extensions; /// -/// DryIoc registration helpers for the S3-compatible (MinIO) storage provider. +/// DryIoc registration helpers for the S3-compatible (MinIO) storage provider. /// public static class S3StorageRegistrationExtensions { diff --git a/src/SquidStd.Storage.S3/README.md b/src/SquidStd.Storage.S3/README.md index f3d74062..ab43cd5b 100644 --- a/src/SquidStd.Storage.S3/README.md +++ b/src/SquidStd.Storage.S3/README.md @@ -35,11 +35,11 @@ await storage.SaveAsync("reports/2026.json", "{}"u8.ToArray()); ## Key types -| Type | Purpose | -|------|---------| -| `S3StorageRegistrationExtensions` | `AddS3Storage(...)` registration. | -| `S3StorageService` | MinIO-backed `IStorageService` with lazy bucket creation. | -| `S3StorageOptions` | Endpoint, credentials, bucket, TLS, region. | +| Type | Purpose | +|-----------------------------------|-----------------------------------------------------------| +| `S3StorageRegistrationExtensions` | `AddS3Storage(...)` registration. | +| `S3StorageService` | MinIO-backed `IStorageService` with lazy bucket creation. | +| `S3StorageOptions` | Endpoint, credentials, bucket, TLS, region. | ## Related diff --git a/src/SquidStd.Storage.S3/Services/S3StorageService.cs b/src/SquidStd.Storage.S3/Services/S3StorageService.cs index 2743d637..59e005af 100644 --- a/src/SquidStd.Storage.S3/Services/S3StorageService.cs +++ b/src/SquidStd.Storage.S3/Services/S3StorageService.cs @@ -8,8 +8,8 @@ namespace SquidStd.Storage.S3.Services; /// -/// S3-compatible backed by the MinIO client. The bucket is created -/// lazily on first use. +/// S3-compatible backed by the MinIO client. The bucket is created +/// lazily on first use. /// public sealed class S3StorageService : IStorageService, IDisposable { @@ -139,9 +139,9 @@ private static IMinioClient CreateClient(S3StorageOptions options) var endpoint = uri.IsDefaultPort ? uri.Host : $"{uri.Host}:{uri.Port}"; var minio = new MinioClient() - .WithEndpoint(endpoint) - .WithCredentials(options.Aws.AccessKey, options.Aws.SecretKey) - .WithSSL(string.Equals(uri.Scheme, "https", StringComparison.OrdinalIgnoreCase)); + .WithEndpoint(endpoint) + .WithCredentials(options.Aws.AccessKey, options.Aws.SecretKey) + .WithSSL(string.Equals(uri.Scheme, "https", StringComparison.OrdinalIgnoreCase)); if (!string.IsNullOrWhiteSpace(options.Aws.Region)) { diff --git a/src/SquidStd.Storage/Extensions/StorageRegistrationExtensions.cs b/src/SquidStd.Storage/Extensions/StorageRegistrationExtensions.cs index 078d0711..8560d300 100644 --- a/src/SquidStd.Storage/Extensions/StorageRegistrationExtensions.cs +++ b/src/SquidStd.Storage/Extensions/StorageRegistrationExtensions.cs @@ -6,7 +6,7 @@ namespace SquidStd.Storage.Extensions; /// -/// DryIoc registration helpers for the local file storage provider. +/// DryIoc registration helpers for the local file storage provider. /// public static class StorageRegistrationExtensions { diff --git a/src/SquidStd.Storage/Internal/StoragePathResolver.cs b/src/SquidStd.Storage/Internal/StoragePathResolver.cs index 2ee06091..8e357434 100644 --- a/src/SquidStd.Storage/Internal/StoragePathResolver.cs +++ b/src/SquidStd.Storage/Internal/StoragePathResolver.cs @@ -1,7 +1,7 @@ namespace SquidStd.Storage.Internal; /// -/// Resolves logical storage keys into paths constrained to one root directory. +/// Resolves logical storage keys into paths constrained to one root directory. /// internal static class StoragePathResolver { @@ -40,8 +40,8 @@ public static string ResolveFilePath(string rootDirectory, string key, string? e var fullPath = Path.GetFullPath(Path.Combine(normalizedRoot, relativePath)); var rootPrefix = normalizedRoot.EndsWith(Path.DirectorySeparatorChar) - ? normalizedRoot - : normalizedRoot + Path.DirectorySeparatorChar; + ? normalizedRoot + : normalizedRoot + Path.DirectorySeparatorChar; if (!fullPath.StartsWith(rootPrefix, StringComparison.Ordinal)) { diff --git a/src/SquidStd.Storage/README.md b/src/SquidStd.Storage/README.md index c3fdf955..a16f0537 100644 --- a/src/SquidStd.Storage/README.md +++ b/src/SquidStd.Storage/README.md @@ -30,11 +30,11 @@ enumerates stored keys (`/`-separated), excluding in-flight temp files. ## Key types -| Type | Purpose | -|------|---------| +| Type | Purpose | +|---------------------------------|---------------------------------------------------------------------------------------------| | `StorageRegistrationExtensions` | `AddFileStorage(...)` registration (file `IStorageService` + YAML `IObjectStorageService`). | -| `FileStorageService` | Filesystem-backed `IStorageService`. | -| `YamlObjectStorageService` | YAML-backed `IObjectStorageService` over a blob store. | +| `FileStorageService` | Filesystem-backed `IStorageService`. | +| `YamlObjectStorageService` | YAML-backed `IObjectStorageService` over a blob store. | ## Related diff --git a/src/SquidStd.Storage/Services/FileStorageService.cs b/src/SquidStd.Storage/Services/FileStorageService.cs index 74118821..94c75417 100644 --- a/src/SquidStd.Storage/Services/FileStorageService.cs +++ b/src/SquidStd.Storage/Services/FileStorageService.cs @@ -6,14 +6,14 @@ namespace SquidStd.Storage.Services; /// -/// Local file-backed binary storage. +/// Local file-backed binary storage. /// public sealed class FileStorageService : IStorageService { private readonly string _rootDirectory; /// - /// Initializes local file storage. + /// Initializes local file storage. /// /// Storage configuration. public FileStorageService(StorageConfig config) @@ -126,7 +126,5 @@ public async ValueTask SaveAsync( } private string ResolvePath(string key) - { - return StoragePathResolver.ResolveFilePath(_rootDirectory, key); - } + => StoragePathResolver.ResolveFilePath(_rootDirectory, key); } diff --git a/src/SquidStd.Storage/Services/YamlObjectStorageService.cs b/src/SquidStd.Storage/Services/YamlObjectStorageService.cs index 619ab586..6bc30acb 100644 --- a/src/SquidStd.Storage/Services/YamlObjectStorageService.cs +++ b/src/SquidStd.Storage/Services/YamlObjectStorageService.cs @@ -5,14 +5,14 @@ namespace SquidStd.Storage.Services; /// -/// YAML object storage built on top of binary storage. +/// YAML object storage built on top of binary storage. /// public sealed class YamlObjectStorageService : IObjectStorageService { private readonly IStorageService _storageService; /// - /// Initializes YAML object storage. + /// Initializes YAML object storage. /// /// Underlying binary storage service. public YamlObjectStorageService(IStorageService storageService) @@ -22,21 +22,15 @@ public YamlObjectStorageService(IStorageService storageService) /// public ValueTask DeleteAsync(string key, CancellationToken cancellationToken = default) - { - return _storageService.DeleteAsync(key, cancellationToken); - } + => _storageService.DeleteAsync(key, cancellationToken); /// public ValueTask ExistsAsync(string key, CancellationToken cancellationToken = default) - { - return _storageService.ExistsAsync(key, cancellationToken); - } + => _storageService.ExistsAsync(key, cancellationToken); /// public IAsyncEnumerable ListKeysAsync(string? prefix = null, CancellationToken cancellationToken = default) - { - return _storageService.ListKeysAsync(prefix, cancellationToken); - } + => _storageService.ListKeysAsync(prefix, cancellationToken); /// public async ValueTask LoadAsync(string key, CancellationToken cancellationToken = default) diff --git a/src/SquidStd.Telemetry.Abstractions/README.md b/src/SquidStd.Telemetry.Abstractions/README.md index 5f1405dd..b475f626 100644 --- a/src/SquidStd.Telemetry.Abstractions/README.md +++ b/src/SquidStd.Telemetry.Abstractions/README.md @@ -25,11 +25,11 @@ captures every `SquidStd.*` source automatically. ## Key types -| Type | Purpose | -|------|---------| +| Type | Purpose | +|--------------------|----------------------------------------------------------------------------| | `TelemetryOptions` | Service name, OTLP endpoint/protocol, sampling and exporter configuration. | -| `OtlpProtocolType` | OTLP transport: gRPC or HTTP. | -| `SquidStdActivity` | Shared `ActivitySource` and the `SquidStd.*` source naming convention. | +| `OtlpProtocolType` | OTLP transport: gRPC or HTTP. | +| `SquidStdActivity` | Shared `ActivitySource` and the `SquidStd.*` source naming convention. | ## License diff --git a/src/SquidStd.Telemetry.Abstractions/SquidStdActivity.cs b/src/SquidStd.Telemetry.Abstractions/SquidStdActivity.cs index aa60d76c..bba30264 100644 --- a/src/SquidStd.Telemetry.Abstractions/SquidStdActivity.cs +++ b/src/SquidStd.Telemetry.Abstractions/SquidStdActivity.cs @@ -3,8 +3,8 @@ namespace SquidStd.Telemetry.Abstractions; /// -/// Well-known SquidStd ActivitySource for app-level custom spans. SquidStd subsystems name their own -/// sources with the "SquidStd." prefix; the OpenTelemetry provider captures them via "SquidStd.*". +/// Well-known SquidStd ActivitySource for app-level custom spans. SquidStd subsystems name their own +/// sources with the "SquidStd." prefix; the OpenTelemetry provider captures them via "SquidStd.*". /// public static class SquidStdActivity { diff --git a/src/SquidStd.Telemetry.OpenTelemetry/Extensions/OpenTelemetryServiceCollectionExtensions.cs b/src/SquidStd.Telemetry.OpenTelemetry/Extensions/OpenTelemetryServiceCollectionExtensions.cs index 6cb28a17..a4203d5d 100644 --- a/src/SquidStd.Telemetry.OpenTelemetry/Extensions/OpenTelemetryServiceCollectionExtensions.cs +++ b/src/SquidStd.Telemetry.OpenTelemetry/Extensions/OpenTelemetryServiceCollectionExtensions.cs @@ -24,7 +24,8 @@ public IServiceCollection AddSquidStdTelemetry(TelemetryOptions options) if (options.EnableTracing) { - builder.WithTracing(tracing => + builder.WithTracing( + tracing => { TelemetryPipeline.ConfigureTracing(tracing, options, true); TelemetryPipeline.AddTraceExporters(tracing, options); @@ -35,7 +36,8 @@ public IServiceCollection AddSquidStdTelemetry(TelemetryOptions options) if (options.EnableMetrics) { services.AddHostedService(); - builder.WithMetrics(metrics => + builder.WithMetrics( + metrics => { TelemetryPipeline.ConfigureMetrics(metrics, options); TelemetryPipeline.AddMetricExporters(metrics, options); diff --git a/src/SquidStd.Telemetry.OpenTelemetry/Internal/MetricsBridgeActivator.cs b/src/SquidStd.Telemetry.OpenTelemetry/Internal/MetricsBridgeActivator.cs index a215f6c7..fd8e8c76 100644 --- a/src/SquidStd.Telemetry.OpenTelemetry/Internal/MetricsBridgeActivator.cs +++ b/src/SquidStd.Telemetry.OpenTelemetry/Internal/MetricsBridgeActivator.cs @@ -4,8 +4,8 @@ namespace SquidStd.Telemetry.OpenTelemetry.Internal; /// -/// Warms the at host start so its observable instruments exist -/// for the MeterProvider's first collection (ASP.NET Core host path). +/// Warms the at host start so its observable instruments exist +/// for the MeterProvider's first collection (ASP.NET Core host path). /// internal sealed class MetricsBridgeActivator : IHostedService { @@ -24,7 +24,5 @@ public Task StartAsync(CancellationToken cancellationToken) } public Task StopAsync(CancellationToken cancellationToken) - { - return Task.CompletedTask; - } + => Task.CompletedTask; } diff --git a/src/SquidStd.Telemetry.OpenTelemetry/Internal/TelemetryPipeline.cs b/src/SquidStd.Telemetry.OpenTelemetry/Internal/TelemetryPipeline.cs index 07623277..4649d044 100644 --- a/src/SquidStd.Telemetry.OpenTelemetry/Internal/TelemetryPipeline.cs +++ b/src/SquidStd.Telemetry.OpenTelemetry/Internal/TelemetryPipeline.cs @@ -11,17 +11,18 @@ namespace SquidStd.Telemetry.OpenTelemetry.Internal; /// -/// Shared OpenTelemetry pipeline configuration, used by both the IContainer and IServiceCollection -/// registration surfaces. Instrumentation/source configuration is split from exporter configuration so -/// tests can reuse the production pipeline and append an in-memory exporter. +/// Shared OpenTelemetry pipeline configuration, used by both the IContainer and IServiceCollection +/// registration surfaces. Instrumentation/source configuration is split from exporter configuration so +/// tests can reuse the production pipeline and append an in-memory exporter. /// internal static class TelemetryPipeline { public static void AddMetricExporters(MeterProviderBuilder builder, TelemetryOptions options) { - builder.AddOtlpExporter(o => + builder.AddOtlpExporter( + o => { - o.Endpoint = new Uri(options.OtlpEndpoint); + o.Endpoint = new(options.OtlpEndpoint); o.Protocol = Map(options.OtlpProtocol); } ); @@ -34,9 +35,10 @@ public static void AddMetricExporters(MeterProviderBuilder builder, TelemetryOpt public static void AddTraceExporters(TracerProviderBuilder builder, TelemetryOptions options) { - builder.AddOtlpExporter(o => + builder.AddOtlpExporter( + o => { - o.Endpoint = new Uri(options.OtlpEndpoint); + o.Endpoint = new(options.OtlpEndpoint); o.Protocol = Map(options.OtlpProtocol); } ); @@ -64,12 +66,10 @@ public static ResourceBuilder BuildResource(TelemetryOptions options) } public static void ConfigureMetrics(MeterProviderBuilder builder, TelemetryOptions options) - { - builder - .SetResourceBuilder(BuildResource(options)) - .AddRuntimeInstrumentation() - .AddMeter(MetricsSnapshotBridge.MeterName); - } + => builder + .SetResourceBuilder(BuildResource(options)) + .AddRuntimeInstrumentation() + .AddMeter(MetricsSnapshotBridge.MeterName); public static void ConfigureTracing(TracerProviderBuilder builder, TelemetryOptions options, bool includeAspNetCore) { @@ -87,7 +87,5 @@ public static void ConfigureTracing(TracerProviderBuilder builder, TelemetryOpti } public static OtlpExportProtocol Map(OtlpProtocolType protocol) - { - return protocol == OtlpProtocolType.HttpProtobuf ? OtlpExportProtocol.HttpProtobuf : OtlpExportProtocol.Grpc; - } + => protocol == OtlpProtocolType.HttpProtobuf ? OtlpExportProtocol.HttpProtobuf : OtlpExportProtocol.Grpc; } diff --git a/src/SquidStd.Telemetry.OpenTelemetry/README.md b/src/SquidStd.Telemetry.OpenTelemetry/README.md index 82c9c48d..1927c527 100644 --- a/src/SquidStd.Telemetry.OpenTelemetry/README.md +++ b/src/SquidStd.Telemetry.OpenTelemetry/README.md @@ -38,12 +38,12 @@ builder.Services.AddSquidStdTelemetry(new TelemetryOptions { ServiceName = "orde ## Key types -| Type | Purpose | -|------|---------| -| `OpenTelemetryContainerExtensions` | `AddSquidStdTelemetry(...)` for the DryIoc/worker host. | -| `OpenTelemetryServiceCollectionExtensions` | `AddSquidStdTelemetry(...)` for the ASP.NET Core host. | -| `TelemetryService` | Configures tracing/metrics providers and exporters from `TelemetryOptions`. | -| `MetricsSnapshotBridge` | Exports the existing SquidStd metrics snapshot to OTel instruments. | +| Type | Purpose | +|--------------------------------------------|-----------------------------------------------------------------------------| +| `OpenTelemetryContainerExtensions` | `AddSquidStdTelemetry(...)` for the DryIoc/worker host. | +| `OpenTelemetryServiceCollectionExtensions` | `AddSquidStdTelemetry(...)` for the ASP.NET Core host. | +| `TelemetryService` | Configures tracing/metrics providers and exporters from `TelemetryOptions`. | +| `MetricsSnapshotBridge` | Exports the existing SquidStd metrics snapshot to OTel instruments. | ## License diff --git a/src/SquidStd.Telemetry.OpenTelemetry/Services/MetricsSnapshotBridge.cs b/src/SquidStd.Telemetry.OpenTelemetry/Services/MetricsSnapshotBridge.cs index a803dbb3..d6e8ec7f 100644 --- a/src/SquidStd.Telemetry.OpenTelemetry/Services/MetricsSnapshotBridge.cs +++ b/src/SquidStd.Telemetry.OpenTelemetry/Services/MetricsSnapshotBridge.cs @@ -6,9 +6,9 @@ namespace SquidStd.Telemetry.OpenTelemetry.Services; /// -/// Bridges the SquidStd metrics snapshot () to OpenTelemetry -/// observable instruments: counters become observable counters, everything else an observable gauge, -/// each callback reading the latest snapshot value for its metric name. +/// Bridges the SquidStd metrics snapshot () to OpenTelemetry +/// observable instruments: counters become observable counters, everything else an observable gauge, +/// each callback reading the latest snapshot value for its metric name. /// public sealed class MetricsSnapshotBridge : IDisposable { @@ -24,7 +24,7 @@ public sealed class MetricsSnapshotBridge : IDisposable public MetricsSnapshotBridge(IMetricsCollectionService metrics) { _metrics = metrics; - _meter = new Meter(MeterName); + _meter = new(MeterName); EnsureInstruments(); } @@ -57,10 +57,10 @@ private IEnumerable> Observe(string name) } var tags = sample.Tags is { Count: > 0 } - ? sample.Tags.Select(kv => new KeyValuePair(kv.Key, kv.Value)).ToArray() - : []; + ? sample.Tags.Select(kv => new KeyValuePair(kv.Key, kv.Value)).ToArray() + : []; - return [new Measurement(sample.Value, tags)]; + return [new(sample.Value, tags)]; } public void Dispose() diff --git a/src/SquidStd.Telemetry.OpenTelemetry/Services/TelemetryService.cs b/src/SquidStd.Telemetry.OpenTelemetry/Services/TelemetryService.cs index 98b73969..d2595a1d 100644 --- a/src/SquidStd.Telemetry.OpenTelemetry/Services/TelemetryService.cs +++ b/src/SquidStd.Telemetry.OpenTelemetry/Services/TelemetryService.cs @@ -9,8 +9,8 @@ namespace SquidStd.Telemetry.OpenTelemetry.Services; /// -/// Owns the OpenTelemetry TracerProvider/MeterProvider for non-web (DryIoc/worker) hosts, building -/// them on start and disposing (flushing) them on stop. Telemetry failures never crash the host. +/// Owns the OpenTelemetry TracerProvider/MeterProvider for non-web (DryIoc/worker) hosts, building +/// them on start and disposing (flushing) them on stop. Telemetry failures never crash the host. /// public sealed class TelemetryService : ISquidStdService, IDisposable { diff --git a/src/SquidStd.Templating/Extensions/TemplatingRegistrationExtensions.cs b/src/SquidStd.Templating/Extensions/TemplatingRegistrationExtensions.cs index 730ed9ee..6c7260fe 100644 --- a/src/SquidStd.Templating/Extensions/TemplatingRegistrationExtensions.cs +++ b/src/SquidStd.Templating/Extensions/TemplatingRegistrationExtensions.cs @@ -6,15 +6,15 @@ namespace SquidStd.Templating.Extensions; /// -/// DryIoc registration helpers for the templating module. +/// DryIoc registration helpers for the templating module. /// public static class TemplatingRegistrationExtensions { extension(IContainer container) { /// - /// Registers the Scriban template renderer as a singleton SquidStd service (so its startup - /// auto-load of templates/*.tmpl runs with the host). Requires a registered DirectoriesConfig. + /// Registers the Scriban template renderer as a singleton SquidStd service (so its startup + /// auto-load of templates/*.tmpl runs with the host). Requires a registered DirectoriesConfig. /// public IContainer AddTemplating() { diff --git a/src/SquidStd.Templating/Interfaces/ITemplateRenderer.cs b/src/SquidStd.Templating/Interfaces/ITemplateRenderer.cs index 48457e9d..f947933f 100644 --- a/src/SquidStd.Templating/Interfaces/ITemplateRenderer.cs +++ b/src/SquidStd.Templating/Interfaces/ITemplateRenderer.cs @@ -1,7 +1,7 @@ namespace SquidStd.Templating.Interfaces; /// -/// Renders Scriban templates from strings or registered named templates. +/// Renders Scriban templates from strings or registered named templates. /// public interface ITemplateRenderer { diff --git a/src/SquidStd.Templating/Services/ScribanTemplateRenderer.cs b/src/SquidStd.Templating/Services/ScribanTemplateRenderer.cs index e066c41e..532cb1d4 100644 --- a/src/SquidStd.Templating/Services/ScribanTemplateRenderer.cs +++ b/src/SquidStd.Templating/Services/ScribanTemplateRenderer.cs @@ -8,8 +8,8 @@ namespace SquidStd.Templating.Services; /// -/// Scriban-backed . Named templates are compiled and cached; ad-hoc -/// strings are parsed per call. On start it auto-loads templates/**/*.tmpl. +/// Scriban-backed . Named templates are compiled and cached; ad-hoc +/// strings are parsed per call. On start it auto-loads templates/**/*.tmpl. /// public sealed class ScribanTemplateRenderer : ITemplateRenderer, ISquidStdService { @@ -45,9 +45,7 @@ public async ValueTask StartAsync(CancellationToken cancellationToken = default) /// public ValueTask StopAsync(CancellationToken cancellationToken = default) - { - return ValueTask.CompletedTask; - } + => ValueTask.CompletedTask; /// public void Register(string name, string template) diff --git a/src/SquidStd.Templating/TemplateException.cs b/src/SquidStd.Templating/TemplateException.cs index 383b31e4..6ceb4f3e 100644 --- a/src/SquidStd.Templating/TemplateException.cs +++ b/src/SquidStd.Templating/TemplateException.cs @@ -1,17 +1,13 @@ namespace SquidStd.Templating; /// -/// Raised when a template fails to parse or render. +/// Raised when a template fails to parse or render. /// public sealed class TemplateException : Exception { public TemplateException(string message) - : base(message) - { - } + : base(message) { } public TemplateException(string message, Exception innerException) - : base(message, innerException) - { - } + : base(message, innerException) { } } diff --git a/src/SquidStd.Tui/Binding/ViewBinder.AutoBind.cs b/src/SquidStd.Tui/Binding/ViewBinder.AutoBind.cs index 55c69aaa..056642ed 100644 --- a/src/SquidStd.Tui/Binding/ViewBinder.AutoBind.cs +++ b/src/SquidStd.Tui/Binding/ViewBinder.AutoBind.cs @@ -31,12 +31,15 @@ public void AutoBind(View view, INotifyPropertyChanged viewModel) { case Button button: BindCommandByName(vmType, viewModel, ConventionNames.CommandName(id), button); + break; case TextField field: - BindStringByName(vmType, viewModel, ConventionNames.MemberName(id), field, twoWay: true, null); + BindStringByName(vmType, viewModel, ConventionNames.MemberName(id), field, true, null); + break; case Label label: - BindStringByName(vmType, viewModel, ConventionNames.MemberName(id), null, twoWay: false, label); + BindStringByName(vmType, viewModel, ConventionNames.MemberName(id), null, false, label); + break; } } @@ -53,7 +56,12 @@ private void BindCommandByName(Type vmType, INotifyPropertyChanged vm, string me } private void BindStringByName( - Type vmType, INotifyPropertyChanged vm, string memberName, TextField? field, bool twoWay, Label? label + Type vmType, + INotifyPropertyChanged vm, + string memberName, + TextField? field, + bool twoWay, + Label? label ) { var property = vmType.GetProperty(memberName, BindingFlags.Public | BindingFlags.Instance); diff --git a/src/SquidStd.Tui/Binding/ViewBinder.Widgets.cs b/src/SquidStd.Tui/Binding/ViewBinder.Widgets.cs index 7755e43e..433555a0 100644 --- a/src/SquidStd.Tui/Binding/ViewBinder.Widgets.cs +++ b/src/SquidStd.Tui/Binding/ViewBinder.Widgets.cs @@ -11,32 +11,26 @@ public sealed partial class ViewBinder /// One-way bind a source string property to a 's text. public void OneWayText(TSource source, Expression> property, Label label) where TSource : INotifyPropertyChanged - { - OneWay(source, property, label, l => l.Text); - } + => OneWay(source, property, label, l => l.Text); /// One-way bind a source string property to a 's title. public void OneWayTitle(TSource source, Expression> property, Window window) where TSource : INotifyPropertyChanged - { - OneWay(source, property, window, w => w.Title); - } + => OneWay(source, property, window, w => w.Title); /// Two-way bind a source string property to a . public void TwoWay(TSource source, Expression> property, TextField field) where TSource : INotifyPropertyChanged - { - // Terminal.Gui 2.4.16 exposes ValueChanged (EventHandler>) - // rather than a TextChanged event. - TwoWay(source, property, field, f => f.Text, callback => field.ValueChanged += (_, _) => callback()); - } + + // Terminal.Gui 2.4.16 exposes ValueChanged (EventHandler>) + // rather than a TextChanged event. + => TwoWay(source, property, field, f => f.Text, callback => field.ValueChanged += (_, _) => callback()); /// Bind a command to a : Accepted triggers it, CanExecute drives Enabled. public void Command(Button button, ICommand command) - { + // Use Accepted (post-accept, non-cancellable) rather than Accepting for this side-effect-only // handler; Terminal.Gui 2.4.16 docs mark subscribing side effects to the cancellable Accepting // phase as incorrect. - Command(command, enabled => button.Enabled = enabled, callback => button.Accepted += (_, _) => callback()); - } + => Command(command, enabled => button.Enabled = enabled, callback => button.Accepted += (_, _) => callback()); } diff --git a/src/SquidStd.Tui/Binding/ViewBinder.cs b/src/SquidStd.Tui/Binding/ViewBinder.cs index 82a9385a..96e051a3 100644 --- a/src/SquidStd.Tui/Binding/ViewBinder.cs +++ b/src/SquidStd.Tui/Binding/ViewBinder.cs @@ -69,30 +69,34 @@ void SourceHandler(object? sender, PropertyChangedEventArgs e) return; } - _marshal(() => - { - using (guard.Enter()) + _marshal( + () => { - applyToTarget(); + using (guard.Enter()) + { + applyToTarget(); + } } - }); + ); } source.PropertyChanged += SourceHandler; _subscriptions.Add(new Unsubscriber(() => source.PropertyChanged -= SourceHandler)); - subscribeTargetChanged(() => - { - if (guard.IsBusy) + subscribeTargetChanged( + () => { - return; - } + if (guard.IsBusy) + { + return; + } - using (guard.Enter()) - { - writeToSource(); + using (guard.Enter()) + { + writeToSource(); + } } - }); + ); } /// @@ -104,20 +108,20 @@ public void Command(ICommand command, Action setEnabled, Action su setEnabled(command.CanExecute(null)); void CanHandler(object? sender, EventArgs e) - { - _marshal(() => setEnabled(command.CanExecute(null))); - } + => _marshal(() => setEnabled(command.CanExecute(null))); command.CanExecuteChanged += CanHandler; _subscriptions.Add(new Unsubscriber(() => command.CanExecuteChanged -= CanHandler)); - subscribeTrigger(() => - { - if (command.CanExecute(null)) + subscribeTrigger( + () => { - command.Execute(null); + if (command.CanExecute(null)) + { + command.Execute(null); + } } - }); + ); } /// Disposes all active subscriptions created by this binder. diff --git a/src/SquidStd.Tui/Dsl/TuiNode.cs b/src/SquidStd.Tui/Dsl/TuiNode.cs index 30a46f5c..3d6d3110 100644 --- a/src/SquidStd.Tui/Dsl/TuiNode.cs +++ b/src/SquidStd.Tui/Dsl/TuiNode.cs @@ -2,6 +2,4 @@ namespace SquidStd.Tui.Dsl; /// Immutable description of a UI element bound to . /// The ViewModel the node's bindings target. -public abstract class TuiNode -{ -} +public abstract class TuiNode { } diff --git a/src/SquidStd.Tui/Dsl/TuiNodeMaterializer.cs b/src/SquidStd.Tui/Dsl/TuiNodeMaterializer.cs index b9abeba6..445dfce8 100644 --- a/src/SquidStd.Tui/Dsl/TuiNodeMaterializer.cs +++ b/src/SquidStd.Tui/Dsl/TuiNodeMaterializer.cs @@ -6,12 +6,16 @@ namespace SquidStd.Tui.Dsl; -/// Turns a tree into a Terminal.Gui view graph, wiring each -/// node's binding through the supplied . +/// +/// Turns a tree into a Terminal.Gui view graph, wiring each +/// node's binding through the supplied . +/// public sealed class TuiNodeMaterializer { - /// Materialises against , registering - /// bindings on , and returns the produced view. + /// + /// Materialises against , registering + /// bindings on , and returns the produced view. + /// public View Materialize(TuiNode node, TViewModel viewModel, ViewBinder binder) where TViewModel : INotifyPropertyChanged { diff --git a/src/SquidStd.Tui/Dsl/UiFactory.cs b/src/SquidStd.Tui/Dsl/UiFactory.cs index e6ad22ea..4d245cfb 100644 --- a/src/SquidStd.Tui/Dsl/UiFactory.cs +++ b/src/SquidStd.Tui/Dsl/UiFactory.cs @@ -13,31 +13,21 @@ public sealed class UiFactory { /// A label one-way bound to a string property. public LabelNode Label(Expression> text) - { - return new LabelNode(text); - } + => new(text); /// A text field bound to a string property (two-way by default). public TextFieldNode TextField(Expression> text, BindMode mode = BindMode.TwoWay) - { - return new TextFieldNode(text, mode); - } + => new(text, mode); /// A button that runs the command resolved from the ViewModel. public ButtonNode Button(string caption, Expression> command) - { - return new ButtonNode(caption, command); - } + => new(caption, command); /// A vertical stack of child nodes. public StackNode VStack(params TuiNode[] children) - { - return new StackNode(StackOrientation.Vertical, children); - } + => new(StackOrientation.Vertical, children); /// A horizontal stack of child nodes. public StackNode HStack(params TuiNode[] children) - { - return new StackNode(StackOrientation.Horizontal, children); - } + => new(StackOrientation.Horizontal, children); } diff --git a/src/SquidStd.Tui/Hosting/TerminalGuiViewHost.cs b/src/SquidStd.Tui/Hosting/TerminalGuiViewHost.cs index 86dcd060..2fd35982 100644 --- a/src/SquidStd.Tui/Hosting/TerminalGuiViewHost.cs +++ b/src/SquidStd.Tui/Hosting/TerminalGuiViewHost.cs @@ -13,7 +13,7 @@ public sealed class TerminalGuiViewHost : ITuiViewHost /// The shell the navigator's screens are added to. Set by the application host before running. public View? Container { get; set; } - /// Adds as a full-size child of and gives it focus. + /// Adds as a full-size child of and gives it focus. public void Show(object view) { if (Container is not null && view is View concrete) @@ -25,7 +25,7 @@ public void Show(object view) } } - /// Removes from and disposes it. + /// Removes from and disposes it. public void Remove(object view) { if (Container is not null && view is View concrete) diff --git a/src/SquidStd.Tui/Interfaces/ITuiNavigator.cs b/src/SquidStd.Tui/Interfaces/ITuiNavigator.cs index 1452ed8f..c1e22713 100644 --- a/src/SquidStd.Tui/Interfaces/ITuiNavigator.cs +++ b/src/SquidStd.Tui/Interfaces/ITuiNavigator.cs @@ -1,5 +1,3 @@ -using SquidStd.Tui; - namespace SquidStd.Tui.Interfaces; /// ViewModel-first navigation over a stack of screens. diff --git a/src/SquidStd.Tui/Internal/ConventionNames.cs b/src/SquidStd.Tui/Internal/ConventionNames.cs index 828200f2..d470ed93 100644 --- a/src/SquidStd.Tui/Internal/ConventionNames.cs +++ b/src/SquidStd.Tui/Internal/ConventionNames.cs @@ -19,7 +19,5 @@ public static string MemberName(string widgetId) } public static string CommandName(string widgetId) - { - return MemberName(widgetId) + "Command"; - } + => MemberName(widgetId) + "Command"; } diff --git a/src/SquidStd.Tui/Internal/ReentryGuard.cs b/src/SquidStd.Tui/Internal/ReentryGuard.cs index 1bf5f195..afb6bc01 100644 --- a/src/SquidStd.Tui/Internal/ReentryGuard.cs +++ b/src/SquidStd.Tui/Internal/ReentryGuard.cs @@ -3,16 +3,11 @@ namespace SquidStd.Tui.Internal; /// Single-threaded reentrancy flag used to stop two-way binding write-back loops. internal sealed class ReentryGuard { - private bool _busy; - - public bool IsBusy - { - get { return _busy; } - } + public bool IsBusy { get; private set; } public IDisposable Enter() { - _busy = true; + IsBusy = true; return new Scope(this); } @@ -27,8 +22,6 @@ public Scope(ReentryGuard owner) } public void Dispose() - { - _owner._busy = false; - } + => _owner.IsBusy = false; } } diff --git a/src/SquidStd.Tui/Internal/TuiViewRegistry.cs b/src/SquidStd.Tui/Internal/TuiViewRegistry.cs index e1e0b5c6..30d0da7b 100644 --- a/src/SquidStd.Tui/Internal/TuiViewRegistry.cs +++ b/src/SquidStd.Tui/Internal/TuiViewRegistry.cs @@ -5,13 +5,11 @@ public sealed class TuiViewRegistry { private readonly Dictionary _map = new(); - /// Registers a mapping from to the that renders it. + /// Registers a mapping from to the that renders it. public void Map(Type viewModelType, Type viewType) - { - _map[viewModelType] = viewType; - } + => _map[viewModelType] = viewType; - /// Returns the view type registered for , or throws if none is registered. + /// Returns the view type registered for , or throws if none is registered. public Type ViewTypeFor(Type viewModelType) { if (_map.TryGetValue(viewModelType, out var viewType)) diff --git a/src/SquidStd.Tui/Navigation/TuiNavigator.cs b/src/SquidStd.Tui/Navigation/TuiNavigator.cs index 3c7a3d93..dec1d83c 100644 --- a/src/SquidStd.Tui/Navigation/TuiNavigator.cs +++ b/src/SquidStd.Tui/Navigation/TuiNavigator.cs @@ -12,10 +12,7 @@ public sealed class TuiNavigator : ITuiNavigator private readonly ITuiViewHost _viewHost; private readonly Stack _stack = new(); - public int Depth - { - get { return _stack.Count; } - } + public int Depth => _stack.Count; public TuiNavigator(IResolver resolver, TuiViewRegistry registry, ITuiViewHost viewHost) { @@ -40,7 +37,7 @@ public async Task NavigateToAsync(CancellationToken cancellationToke await _stack.Peek().ViewModel.OnDeactivatedAsync(); } - _stack.Push(new Screen(viewModel, view)); + _stack.Push(new(viewModel, view)); _viewHost.Show(view); await viewModel.OnActivatedAsync(); diff --git a/src/SquidStd.Tui/README.md b/src/SquidStd.Tui/README.md index 3452b06e..e3cdb69e 100644 --- a/src/SquidStd.Tui/README.md +++ b/src/SquidStd.Tui/README.md @@ -70,14 +70,14 @@ public sealed class CounterView : TuiComposedView ## Key types -| Type | Purpose | -|------|---------| -| `TuiViewModel` | ViewModel base (ObservableObject + activation hooks + `Navigator`). | -| `TuiView` | View base: a Terminal.Gui `Window` with a typed ViewModel and a binder. | -| `ViewBinder` | One-way / two-way / command binding; fluent typed + `AutoBind` by convention. | -| `ITuiNavigator` | ViewModel-first stack navigation (`NavigateToAsync`, `BackAsync`). | -| `TuiApplicationHost` | Boots the Terminal.Gui loop with a root ViewModel. | -| `RegisterTui()` / `RegisterView<,>()` | DryIoc registration. | +| Type | Purpose | +|---------------------------------------|-------------------------------------------------------------------------------| +| `TuiViewModel` | ViewModel base (ObservableObject + activation hooks + `Navigator`). | +| `TuiView` | View base: a Terminal.Gui `Window` with a typed ViewModel and a binder. | +| `ViewBinder` | One-way / two-way / command binding; fluent typed + `AutoBind` by convention. | +| `ITuiNavigator` | ViewModel-first stack navigation (`NavigateToAsync`, `BackAsync`). | +| `TuiApplicationHost` | Boots the Terminal.Gui loop with a root ViewModel. | +| `RegisterTui()` / `RegisterView<,>()` | DryIoc registration. | ## License diff --git a/src/SquidStd.Tui/TuiComposedView.cs b/src/SquidStd.Tui/TuiComposedView.cs index 85c26c7d..0f85d711 100644 --- a/src/SquidStd.Tui/TuiComposedView.cs +++ b/src/SquidStd.Tui/TuiComposedView.cs @@ -20,13 +20,9 @@ public abstract class TuiComposedView : TuiView /// Returns the declarative node tree for this view. protected abstract TuiNode Compose(); - protected sealed override void BuildLayout() - { - } + protected sealed override void BuildLayout() { } - protected sealed override void Bind(ViewBinder binder) - { - } + protected sealed override void Bind(ViewBinder binder) { } protected override void OnInitialize(ViewBinder binder) { diff --git a/src/SquidStd.Tui/TuiView.cs b/src/SquidStd.Tui/TuiView.cs index e7b1a306..c39e6908 100644 --- a/src/SquidStd.Tui/TuiView.cs +++ b/src/SquidStd.Tui/TuiView.cs @@ -21,18 +21,14 @@ public abstract class TuiView : Window, ITuiView protected TuiView() { - _binder = new ViewBinder(Application.Invoke); + _binder = new(Application.Invoke); } void ITuiView.Bind(object viewModel) - { - ViewModel = (TViewModel)viewModel; - } + => ViewModel = (TViewModel)viewModel; void ITuiView.Initialize() - { - OnInitialize(_binder); - } + => OnInitialize(_binder); /// /// Builds the view. The default runs then ; the diff --git a/src/SquidStd.Tui/TuiViewModel.cs b/src/SquidStd.Tui/TuiViewModel.cs index 6566658b..84414e89 100644 --- a/src/SquidStd.Tui/TuiViewModel.cs +++ b/src/SquidStd.Tui/TuiViewModel.cs @@ -15,13 +15,9 @@ public abstract class TuiViewModel : ObservableObject /// Invoked after the screen becomes active. Default is a no-op. public virtual ValueTask OnActivatedAsync() - { - return ValueTask.CompletedTask; - } + => ValueTask.CompletedTask; /// Invoked after the screen is removed or hidden. Default is a no-op. public virtual ValueTask OnDeactivatedAsync() - { - return ValueTask.CompletedTask; - } + => ValueTask.CompletedTask; } diff --git a/src/SquidStd.Vfs.Abstractions/README.md b/src/SquidStd.Vfs.Abstractions/README.md index a68e0c3b..fa75b934 100644 --- a/src/SquidStd.Vfs.Abstractions/README.md +++ b/src/SquidStd.Vfs.Abstractions/README.md @@ -1,6 +1,7 @@

SquidStd.Vfs.Abstractions

-Virtual filesystem contracts for SquidStd: a path-based file/directory abstraction implemented by physical, in-memory and zip backends. +Virtual filesystem contracts for SquidStd: a path-based file/directory abstraction implemented by physical, in-memory and zip +backends. ## Install @@ -10,12 +11,12 @@ dotnet add package SquidStd.Vfs.Abstractions ## Key types -| Type | Purpose | -|------|---------| -| `IVirtualFileSystem` | Path-based filesystem over a pluggable backend (directory, zip, encrypted container): exists, read/write bytes, open read/write streams, delete and list entries. | -| `ILockableFileSystem` | An `IVirtualFileSystem` that stays locked until unlocked with a passphrase (e.g. an encrypted vault), exposing `IsUnlocked`, `Unlock` and `Lock`. | -| `VfsPath` | Static helper that normalizes logical paths to forward-slash, root-relative form and rejects `.`/`..` traversal segments. | -| `VfsEntry` | Record describing a listed entry: its logical path, byte size and last-modified UTC timestamp. | +| Type | Purpose | +|-----------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `IVirtualFileSystem` | Path-based filesystem over a pluggable backend (directory, zip, encrypted container): exists, read/write bytes, open read/write streams, delete and list entries. | +| `ILockableFileSystem` | An `IVirtualFileSystem` that stays locked until unlocked with a passphrase (e.g. an encrypted vault), exposing `IsUnlocked`, `Unlock` and `Lock`. | +| `VfsPath` | Static helper that normalizes logical paths to forward-slash, root-relative form and rejects `.`/`..` traversal segments. | +| `VfsEntry` | Record describing a listed entry: its logical path, byte size and last-modified UTC timestamp. | ## Related diff --git a/src/SquidStd.Vfs.Abstractions/VfsPath.cs b/src/SquidStd.Vfs.Abstractions/VfsPath.cs index 4c3db181..07e92cc8 100644 --- a/src/SquidStd.Vfs.Abstractions/VfsPath.cs +++ b/src/SquidStd.Vfs.Abstractions/VfsPath.cs @@ -9,7 +9,7 @@ public static string Normalize(string path) ArgumentException.ThrowIfNullOrWhiteSpace(path); var segments = path.Replace('\\', '/') - .Split('/', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + .Split('/', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); foreach (var segment in segments) { diff --git a/src/SquidStd.Vfs/README.md b/src/SquidStd.Vfs/README.md index 01137a3e..82c5e888 100644 --- a/src/SquidStd.Vfs/README.md +++ b/src/SquidStd.Vfs/README.md @@ -51,14 +51,14 @@ Logical paths are normalized (forward slashes, root-relative) and reject `..` tr ## Key types -| Type | Purpose | -|------|---------| -| `IVirtualFileSystem` | Path-based filesystem over a pluggable backend. | -| `PhysicalFileSystem` | Maps logical paths onto a real directory tree. | -| `ZipFileSystem` | A single `.zip` archive opened in update mode; `IAsyncDisposable`. | -| `InMemoryFileSystem` | Ephemeral, in-process; handy for tests and as a decorator target. | -| `VfsDirectories` | Named directory layout (`DirectoriesConfig` analogue) over any backend. | -| `RegisterVfsExtensions` | `RegisterVfs(...)` registration. | +| Type | Purpose | +|-------------------------|-------------------------------------------------------------------------| +| `IVirtualFileSystem` | Path-based filesystem over a pluggable backend. | +| `PhysicalFileSystem` | Maps logical paths onto a real directory tree. | +| `ZipFileSystem` | A single `.zip` archive opened in update mode; `IAsyncDisposable`. | +| `InMemoryFileSystem` | Ephemeral, in-process; handy for tests and as a decorator target. | +| `VfsDirectories` | Named directory layout (`DirectoriesConfig` analogue) over any backend. | +| `RegisterVfsExtensions` | `RegisterVfs(...)` registration. | ## Related diff --git a/src/SquidStd.Vfs/Services/InMemoryFileSystem.cs b/src/SquidStd.Vfs/Services/InMemoryFileSystem.cs index 964002f3..a6782a7b 100644 --- a/src/SquidStd.Vfs/Services/InMemoryFileSystem.cs +++ b/src/SquidStd.Vfs/Services/InMemoryFileSystem.cs @@ -1,33 +1,31 @@ using System.Collections.Concurrent; using System.Runtime.CompilerServices; +using SquidStd.Vfs.Abstractions; using SquidStd.Vfs.Abstractions.Data; using SquidStd.Vfs.Abstractions.Interfaces; -using SquidStd.Vfs.Abstractions; namespace SquidStd.Vfs.Services; /// An in-memory virtual filesystem. Ephemeral; useful for tests and as a backend decorator target. public sealed class InMemoryFileSystem : IVirtualFileSystem { - private readonly ConcurrentDictionary _files = new( - StringComparer.Ordinal - ); + private readonly ConcurrentDictionary _files = + new(StringComparer.Ordinal); public ValueTask ExistsAsync(string path, CancellationToken cancellationToken = default) - { - return ValueTask.FromResult(_files.ContainsKey(VfsPath.Normalize(path))); - } + => ValueTask.FromResult(_files.ContainsKey(VfsPath.Normalize(path))); public ValueTask ReadAllBytesAsync(string path, CancellationToken cancellationToken = default) - { + // Return a copy: the stored array must not be aliased to callers, who may mutate what they read. - return ValueTask.FromResult( + => ValueTask.FromResult( _files.TryGetValue(VfsPath.Normalize(path), out var entry) ? (byte[])entry.Data.Clone() : null ); - } public ValueTask WriteAllBytesAsync( - string path, ReadOnlyMemory data, CancellationToken cancellationToken = default + string path, + ReadOnlyMemory data, + CancellationToken cancellationToken = default ) { _files[VfsPath.Normalize(path)] = (data.ToArray(), DateTimeOffset.UtcNow); @@ -37,24 +35,21 @@ public ValueTask WriteAllBytesAsync( public async Task OpenReadAsync(string path, CancellationToken cancellationToken = default) { - var data = await ReadAllBytesAsync(path, cancellationToken).ConfigureAwait(false) - ?? throw new FileNotFoundException($"No file at '{path}'.", path); + var data = await ReadAllBytesAsync(path, cancellationToken).ConfigureAwait(false) ?? + throw new FileNotFoundException($"No file at '{path}'.", path); - return new MemoryStream(data, writable: false); + return new MemoryStream(data, false); } public Task OpenWriteAsync(string path, CancellationToken cancellationToken = default) - { - return Task.FromResult(new WriteBackStream(this, path)); - } + => Task.FromResult(new WriteBackStream(this, path)); public ValueTask DeleteAsync(string path, CancellationToken cancellationToken = default) - { - return ValueTask.FromResult(_files.TryRemove(VfsPath.Normalize(path), out _)); - } + => ValueTask.FromResult(_files.TryRemove(VfsPath.Normalize(path), out _)); public async IAsyncEnumerable ListAsync( - string? prefix = null, [EnumeratorCancellation] CancellationToken cancellationToken = default + string? prefix = null, + [EnumeratorCancellation] CancellationToken cancellationToken = default ) { var normalizedPrefix = string.IsNullOrEmpty(prefix) ? null : VfsPath.Normalize(prefix); @@ -66,7 +61,7 @@ public async IAsyncEnumerable ListAsync( continue; } - yield return new VfsEntry(path, entry.Data.Length, entry.Modified); + yield return new(path, entry.Data.Length, entry.Modified); await Task.CompletedTask; } diff --git a/src/SquidStd.Vfs/Services/PhysicalFileSystem.cs b/src/SquidStd.Vfs/Services/PhysicalFileSystem.cs index 4de9584c..55b74f50 100644 --- a/src/SquidStd.Vfs/Services/PhysicalFileSystem.cs +++ b/src/SquidStd.Vfs/Services/PhysicalFileSystem.cs @@ -1,7 +1,7 @@ using System.Runtime.CompilerServices; +using SquidStd.Vfs.Abstractions; using SquidStd.Vfs.Abstractions.Data; using SquidStd.Vfs.Abstractions.Interfaces; -using SquidStd.Vfs.Abstractions; namespace SquidStd.Vfs.Services; @@ -18,21 +18,21 @@ public PhysicalFileSystem(string root) } public ValueTask ExistsAsync(string path, CancellationToken cancellationToken = default) - { - return ValueTask.FromResult(File.Exists(Resolve(path))); - } + => ValueTask.FromResult(File.Exists(Resolve(path))); public async ValueTask ReadAllBytesAsync(string path, CancellationToken cancellationToken = default) { var full = Resolve(path); return File.Exists(full) - ? await File.ReadAllBytesAsync(full, cancellationToken).ConfigureAwait(false) - : null; + ? await File.ReadAllBytesAsync(full, cancellationToken).ConfigureAwait(false) + : null; } public async ValueTask WriteAllBytesAsync( - string path, ReadOnlyMemory data, CancellationToken cancellationToken = default + string path, + ReadOnlyMemory data, + CancellationToken cancellationToken = default ) { var full = Resolve(path); @@ -75,7 +75,8 @@ public ValueTask DeleteAsync(string path, CancellationToken cancellationTo } public async IAsyncEnumerable ListAsync( - string? prefix = null, [EnumeratorCancellation] CancellationToken cancellationToken = default + string? prefix = null, + [EnumeratorCancellation] CancellationToken cancellationToken = default ) { if (!Directory.Exists(_root)) @@ -97,7 +98,8 @@ public async IAsyncEnumerable ListAsync( } var info = new FileInfo(file); - yield return new VfsEntry(logical, info.Length, info.LastWriteTimeUtc); + + yield return new(logical, info.Length, info.LastWriteTimeUtc); await Task.CompletedTask; } diff --git a/src/SquidStd.Vfs/Services/VfsDirectories.cs b/src/SquidStd.Vfs/Services/VfsDirectories.cs index 531a3a57..2078bd53 100644 --- a/src/SquidStd.Vfs/Services/VfsDirectories.cs +++ b/src/SquidStd.Vfs/Services/VfsDirectories.cs @@ -1,5 +1,5 @@ -using SquidStd.Vfs.Abstractions.Interfaces; using SquidStd.Vfs.Abstractions; +using SquidStd.Vfs.Abstractions.Interfaces; namespace SquidStd.Vfs.Services; @@ -37,7 +37,5 @@ public string GetPath(string directoryName) /// Combines a named directory with a relative sub-path into a normalized logical path. public string Combine(string directoryName, string relativePath) - { - return VfsPath.Normalize(GetPath(directoryName) + "/" + relativePath); - } + => VfsPath.Normalize(GetPath(directoryName) + "/" + relativePath); } diff --git a/src/SquidStd.Vfs/Services/ZipFileSystem.cs b/src/SquidStd.Vfs/Services/ZipFileSystem.cs index a080fa81..63207e34 100644 --- a/src/SquidStd.Vfs/Services/ZipFileSystem.cs +++ b/src/SquidStd.Vfs/Services/ZipFileSystem.cs @@ -1,8 +1,8 @@ using System.IO.Compression; using System.Runtime.CompilerServices; +using SquidStd.Vfs.Abstractions; using SquidStd.Vfs.Abstractions.Data; using SquidStd.Vfs.Abstractions.Interfaces; -using SquidStd.Vfs.Abstractions; namespace SquidStd.Vfs.Services; @@ -16,14 +16,12 @@ public sealed class ZipFileSystem : IVirtualFileSystem, IAsyncDisposable, IDispo public ZipFileSystem(string path) { ArgumentException.ThrowIfNullOrWhiteSpace(path); - _file = new FileStream(path, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None); - _archive = new ZipArchive(_file, ZipArchiveMode.Update, leaveOpen: true); + _file = new(path, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None); + _archive = new(_file, ZipArchiveMode.Update, true); } public ValueTask ExistsAsync(string path, CancellationToken cancellationToken = default) - { - return ValueTask.FromResult(_archive.GetEntry(VfsPath.Normalize(path)) is not null); - } + => ValueTask.FromResult(_archive.GetEntry(VfsPath.Normalize(path)) is not null); public async ValueTask ReadAllBytesAsync(string path, CancellationToken cancellationToken = default) { @@ -42,7 +40,9 @@ public ValueTask ExistsAsync(string path, CancellationToken cancellationTo } public async ValueTask WriteAllBytesAsync( - string path, ReadOnlyMemory data, CancellationToken cancellationToken = default + string path, + ReadOnlyMemory data, + CancellationToken cancellationToken = default ) { var name = VfsPath.Normalize(path); @@ -59,16 +59,14 @@ public async ValueTask WriteAllBytesAsync( public async Task OpenReadAsync(string path, CancellationToken cancellationToken = default) { - var data = await ReadAllBytesAsync(path, cancellationToken).ConfigureAwait(false) - ?? throw new FileNotFoundException($"No file at '{path}'.", path); + var data = await ReadAllBytesAsync(path, cancellationToken).ConfigureAwait(false) ?? + throw new FileNotFoundException($"No file at '{path}'.", path); - return new MemoryStream(data, writable: false); + return new MemoryStream(data, false); } public Task OpenWriteAsync(string path, CancellationToken cancellationToken = default) - { - return Task.FromResult(new WriteBackStream(this, path)); - } + => Task.FromResult(new WriteBackStream(this, path)); public ValueTask DeleteAsync(string path, CancellationToken cancellationToken = default) { @@ -87,7 +85,8 @@ public ValueTask DeleteAsync(string path, CancellationToken cancellationTo } public async IAsyncEnumerable ListAsync( - string? prefix = null, [EnumeratorCancellation] CancellationToken cancellationToken = default + string? prefix = null, + [EnumeratorCancellation] CancellationToken cancellationToken = default ) { var normalizedPrefix = string.IsNullOrEmpty(prefix) ? null : VfsPath.Normalize(prefix); @@ -99,7 +98,7 @@ public async IAsyncEnumerable ListAsync( continue; } - yield return new VfsEntry(entry.FullName, EntrySize(entry), entry.LastWriteTime); + yield return new(entry.FullName, EntrySize(entry), entry.LastWriteTime); await Task.CompletedTask; } diff --git a/src/SquidStd.Workers.Abstractions/Data/JobRequest.cs b/src/SquidStd.Workers.Abstractions/Data/JobRequest.cs index cc9477e4..0effc8c4 100644 --- a/src/SquidStd.Workers.Abstractions/Data/JobRequest.cs +++ b/src/SquidStd.Workers.Abstractions/Data/JobRequest.cs @@ -1,7 +1,7 @@ namespace SquidStd.Workers.Abstractions.Data; /// -/// A unit of work the manager enqueues on the jobs queue for a worker to execute. +/// A unit of work the manager enqueues on the jobs queue for a worker to execute. /// /// Logical name of the job, used by the worker to pick a handler. /// Opaque string key/value arguments for the job. diff --git a/src/SquidStd.Workers.Abstractions/Data/WorkerHeartbeat.cs b/src/SquidStd.Workers.Abstractions/Data/WorkerHeartbeat.cs index 622fc150..a9c75ba7 100644 --- a/src/SquidStd.Workers.Abstractions/Data/WorkerHeartbeat.cs +++ b/src/SquidStd.Workers.Abstractions/Data/WorkerHeartbeat.cs @@ -3,13 +3,13 @@ namespace SquidStd.Workers.Abstractions.Data; /// -/// A liveness signal a worker publishes on the heartbeat topic at a fixed interval. +/// A liveness signal a worker publishes on the heartbeat topic at a fixed interval. /// /// Stable identity of the reporting worker. /// UTC time the heartbeat was produced. /// -/// Self-reported status ( when no job is running, else -/// ). +/// Self-reported status ( when no job is running, else +/// ). /// /// Number of jobs currently in progress on this worker. /// Maximum number of jobs the worker runs at once. diff --git a/src/SquidStd.Workers.Abstractions/Data/WorkerInfo.cs b/src/SquidStd.Workers.Abstractions/Data/WorkerInfo.cs index 036d7090..86d5f548 100644 --- a/src/SquidStd.Workers.Abstractions/Data/WorkerInfo.cs +++ b/src/SquidStd.Workers.Abstractions/Data/WorkerInfo.cs @@ -3,7 +3,7 @@ namespace SquidStd.Workers.Abstractions.Data; /// -/// The manager-side view of a worker, folded from incoming heartbeats. +/// The manager-side view of a worker, folded from incoming heartbeats. /// /// Stable identity of the worker. /// Last known status; the manager sets on missed heartbeats. diff --git a/src/SquidStd.Workers.Abstractions/Types/WorkerStatusType.cs b/src/SquidStd.Workers.Abstractions/Types/WorkerStatusType.cs index 449b32dd..d3aa947c 100644 --- a/src/SquidStd.Workers.Abstractions/Types/WorkerStatusType.cs +++ b/src/SquidStd.Workers.Abstractions/Types/WorkerStatusType.cs @@ -1,8 +1,8 @@ namespace SquidStd.Workers.Abstractions.Types; /// -/// Lifecycle status of a worker. Workers report or ; -/// the manager assigns when a worker's heartbeats stop arriving. +/// Lifecycle status of a worker. Workers report or ; +/// the manager assigns when a worker's heartbeats stop arriving. /// public enum WorkerStatusType { diff --git a/src/SquidStd.Workers.Abstractions/WorkerChannels.cs b/src/SquidStd.Workers.Abstractions/WorkerChannels.cs index 899972c2..6514a147 100644 --- a/src/SquidStd.Workers.Abstractions/WorkerChannels.cs +++ b/src/SquidStd.Workers.Abstractions/WorkerChannels.cs @@ -1,8 +1,8 @@ namespace SquidStd.Workers.Abstractions; /// -/// Default names of the messaging channels the workers system uses. Shared so the manager -/// and workers agree out of the box; either side may override them via its own configuration. +/// Default names of the messaging channels the workers system uses. Shared so the manager +/// and workers agree out of the box; either side may override them via its own configuration. /// public static class WorkerChannels { diff --git a/src/SquidStd.Workers.Manager/Data/Config/WorkerManagerConfig.cs b/src/SquidStd.Workers.Manager/Data/Config/WorkerManagerConfig.cs index 96e577db..beef18cd 100644 --- a/src/SquidStd.Workers.Manager/Data/Config/WorkerManagerConfig.cs +++ b/src/SquidStd.Workers.Manager/Data/Config/WorkerManagerConfig.cs @@ -3,7 +3,7 @@ namespace SquidStd.Workers.Manager.Data.Config; /// -/// Configuration for the worker manager (config section "workerManager"). +/// Configuration for the worker manager (config section "workerManager"). /// public sealed class WorkerManagerConfig { diff --git a/src/SquidStd.Workers.Manager/Data/EnqueueJobRequest.cs b/src/SquidStd.Workers.Manager/Data/EnqueueJobRequest.cs index d78e30bf..8869f5f7 100644 --- a/src/SquidStd.Workers.Manager/Data/EnqueueJobRequest.cs +++ b/src/SquidStd.Workers.Manager/Data/EnqueueJobRequest.cs @@ -1,7 +1,7 @@ namespace SquidStd.Workers.Manager.Data; /// -/// Request body for enqueuing a job via the manager's HTTP surface. +/// Request body for enqueuing a job via the manager's HTTP surface. /// /// Logical name of the job. /// Optional string key/value arguments; treated as empty when null. diff --git a/src/SquidStd.Workers.Manager/Data/Events/WorkerStatusChangedEvent.cs b/src/SquidStd.Workers.Manager/Data/Events/WorkerStatusChangedEvent.cs index c9e43cae..e2922b8c 100644 --- a/src/SquidStd.Workers.Manager/Data/Events/WorkerStatusChangedEvent.cs +++ b/src/SquidStd.Workers.Manager/Data/Events/WorkerStatusChangedEvent.cs @@ -4,8 +4,8 @@ namespace SquidStd.Workers.Manager.Data.Events; /// -/// Published on the event bus when a worker's status changes: discovered ( null), -/// gone Offline (via the sweep), or returned (Offline → Idle/Busy). +/// Published on the event bus when a worker's status changes: discovered ( null), +/// gone Offline (via the sweep), or returned (Offline → Idle/Busy). /// /// The worker whose status changed. /// Previous status, or null when the worker was just discovered. diff --git a/src/SquidStd.Workers.Manager/Endpoints/WorkerManagerEndpoints.cs b/src/SquidStd.Workers.Manager/Endpoints/WorkerManagerEndpoints.cs index 0fcd2174..d3928537 100644 --- a/src/SquidStd.Workers.Manager/Endpoints/WorkerManagerEndpoints.cs +++ b/src/SquidStd.Workers.Manager/Endpoints/WorkerManagerEndpoints.cs @@ -7,7 +7,7 @@ namespace SquidStd.Workers.Manager.Endpoints; /// -/// Typed minimal-API handlers for the worker manager surface. Static so they can be unit-tested directly. +/// Typed minimal-API handlers for the worker manager surface. Static so they can be unit-tested directly. /// public static class WorkerManagerEndpoints { @@ -52,7 +52,5 @@ public static Results, NotFound> GetWorker(string id, IWorkerRegi /// Lists all known workers. public static Ok> GetWorkers(IWorkerRegistry registry) - { - return TypedResults.Ok(registry.GetAll()); - } + => TypedResults.Ok(registry.GetAll()); } diff --git a/src/SquidStd.Workers.Manager/Extensions/WorkerManagerEndpointsExtensions.cs b/src/SquidStd.Workers.Manager/Extensions/WorkerManagerEndpointsExtensions.cs index 3a9a88ad..56733c6c 100644 --- a/src/SquidStd.Workers.Manager/Extensions/WorkerManagerEndpointsExtensions.cs +++ b/src/SquidStd.Workers.Manager/Extensions/WorkerManagerEndpointsExtensions.cs @@ -5,14 +5,14 @@ namespace SquidStd.Workers.Manager.Extensions; /// -/// Maps the opt-in worker manager HTTP endpoints. +/// Maps the opt-in worker manager HTTP endpoints. /// public static class WorkerManagerEndpointsExtensions { extension(IEndpointRouteBuilder endpoints) { /// - /// Maps GET /workers, GET /workers/{id}, and POST /jobs. + /// Maps GET /workers, GET /workers/{id}, and POST /jobs. /// public IEndpointRouteBuilder MapWorkerManagerEndpoints() { diff --git a/src/SquidStd.Workers.Manager/Extensions/WorkerManagerRegistrationExtensions.cs b/src/SquidStd.Workers.Manager/Extensions/WorkerManagerRegistrationExtensions.cs index 12500e49..a75b9aae 100644 --- a/src/SquidStd.Workers.Manager/Extensions/WorkerManagerRegistrationExtensions.cs +++ b/src/SquidStd.Workers.Manager/Extensions/WorkerManagerRegistrationExtensions.cs @@ -10,15 +10,15 @@ namespace SquidStd.Workers.Manager.Extensions; /// -/// DryIoc registration helpers for the worker manager. +/// DryIoc registration helpers for the worker manager. /// public static class WorkerManagerRegistrationExtensions { extension(IContainer container) { /// - /// Registers the worker manager: config section, registry, job scheduler, the collector and sweep - /// lifecycle services, and the timer-wheel pump (only if it is not already registered). + /// Registers the worker manager: config section, registry, job scheduler, the collector and sweep + /// lifecycle services, and the timer-wheel pump (only if it is not already registered). /// public IContainer AddWorkerManager() { diff --git a/src/SquidStd.Workers.Manager/Interfaces/IJobScheduler.cs b/src/SquidStd.Workers.Manager/Interfaces/IJobScheduler.cs index e5d68888..337d0f5f 100644 --- a/src/SquidStd.Workers.Manager/Interfaces/IJobScheduler.cs +++ b/src/SquidStd.Workers.Manager/Interfaces/IJobScheduler.cs @@ -1,7 +1,7 @@ namespace SquidStd.Workers.Manager.Interfaces; /// -/// Enqueues jobs for workers to consume. +/// Enqueues jobs for workers to consume. /// public interface IJobScheduler { diff --git a/src/SquidStd.Workers.Manager/Interfaces/IWorkerRegistry.cs b/src/SquidStd.Workers.Manager/Interfaces/IWorkerRegistry.cs index 39eff65f..8c46d819 100644 --- a/src/SquidStd.Workers.Manager/Interfaces/IWorkerRegistry.cs +++ b/src/SquidStd.Workers.Manager/Interfaces/IWorkerRegistry.cs @@ -3,7 +3,7 @@ namespace SquidStd.Workers.Manager.Interfaces; /// -/// Read access to the manager's in-memory view of known workers. +/// Read access to the manager's in-memory view of known workers. /// public interface IWorkerRegistry { diff --git a/src/SquidStd.Workers.Manager/Services/HeartbeatCollectorService.cs b/src/SquidStd.Workers.Manager/Services/HeartbeatCollectorService.cs index 86023ff2..e58c69dd 100644 --- a/src/SquidStd.Workers.Manager/Services/HeartbeatCollectorService.cs +++ b/src/SquidStd.Workers.Manager/Services/HeartbeatCollectorService.cs @@ -9,8 +9,8 @@ namespace SquidStd.Workers.Manager.Services; /// -/// Subscribes to the heartbeat topic and folds each into the registry, -/// publishing any resulting status transition on the event bus. +/// Subscribes to the heartbeat topic and folds each into the registry, +/// publishing any resulting status transition on the event bus. /// public sealed class HeartbeatCollectorService : ISquidStdService { @@ -32,8 +32,8 @@ WorkerManagerConfig config _registry = registry; _eventBus = eventBus; _topicName = string.IsNullOrWhiteSpace(config.HeartbeatTopic) - ? WorkerChannels.HeartbeatTopic - : config.HeartbeatTopic; + ? WorkerChannels.HeartbeatTopic + : config.HeartbeatTopic; } /// diff --git a/src/SquidStd.Workers.Manager/Services/JobScheduler.cs b/src/SquidStd.Workers.Manager/Services/JobScheduler.cs index c45dac81..646c7b5f 100644 --- a/src/SquidStd.Workers.Manager/Services/JobScheduler.cs +++ b/src/SquidStd.Workers.Manager/Services/JobScheduler.cs @@ -7,7 +7,7 @@ namespace SquidStd.Workers.Manager.Services; /// -/// Default : publishes s onto the configured jobs queue. +/// Default : publishes s onto the configured jobs queue. /// public sealed class JobScheduler : IJobScheduler { @@ -26,13 +26,9 @@ public Task EnqueueAsync( IReadOnlyDictionary parameters, CancellationToken cancellationToken = default ) - { - return _queue.PublishAsync(_queueName, new JobRequest(jobName, parameters), cancellationToken); - } + => _queue.PublishAsync(_queueName, new JobRequest(jobName, parameters), cancellationToken); /// public Task EnqueueAsync(string jobName, CancellationToken cancellationToken = default) - { - return EnqueueAsync(jobName, new Dictionary(), cancellationToken); - } + => EnqueueAsync(jobName, new Dictionary(), cancellationToken); } diff --git a/src/SquidStd.Workers.Manager/Services/WorkerOfflineSweepService.cs b/src/SquidStd.Workers.Manager/Services/WorkerOfflineSweepService.cs index 0937723a..c2abe81c 100644 --- a/src/SquidStd.Workers.Manager/Services/WorkerOfflineSweepService.cs +++ b/src/SquidStd.Workers.Manager/Services/WorkerOfflineSweepService.cs @@ -7,8 +7,8 @@ namespace SquidStd.Workers.Manager.Services; /// -/// Periodically marks workers Offline whose heartbeats have stopped, using the timer wheel -/// (), and publishes the resulting transitions. +/// Periodically marks workers Offline whose heartbeats have stopped, using the timer wheel +/// (), and publishes the resulting transitions. /// public sealed class WorkerOfflineSweepService : ISquidStdService { @@ -83,7 +83,5 @@ public async Task RunSweepAsync() } private void OnTick() - { - _ = RunSweepAsync(); - } + => _ = RunSweepAsync(); } diff --git a/src/SquidStd.Workers.Manager/Services/WorkerRegistry.cs b/src/SquidStd.Workers.Manager/Services/WorkerRegistry.cs index 64cf72c6..509e2bb1 100644 --- a/src/SquidStd.Workers.Manager/Services/WorkerRegistry.cs +++ b/src/SquidStd.Workers.Manager/Services/WorkerRegistry.cs @@ -7,8 +7,8 @@ namespace SquidStd.Workers.Manager.Services; /// -/// In-memory registry of workers, folded from heartbeats. Pure: it returns status transitions for the -/// caller to publish, and never touches the event bus or a real clock (the sweep takes the time as a parameter). +/// In-memory registry of workers, folded from heartbeats. Pure: it returns status transitions for the +/// caller to publish, and never touches the event bus or a real clock (the sweep takes the time as a parameter). /// public sealed class WorkerRegistry : IWorkerRegistry { @@ -41,8 +41,8 @@ public IReadOnlyCollection GetAll() } /// - /// Folds a heartbeat into the registry. Returns a transition only when the worker is newly discovered - /// or has returned from ; otherwise null. + /// Folds a heartbeat into the registry. Returns a transition only when the worker is newly discovered + /// or has returned from ; otherwise null. /// public WorkerStatusChangedEvent? Record(WorkerHeartbeat heartbeat) { @@ -52,7 +52,7 @@ public IReadOnlyCollection GetAll() { if (!_workers.TryGetValue(heartbeat.WorkerId, out var existing)) { - _workers[heartbeat.WorkerId] = new WorkerInfo( + _workers[heartbeat.WorkerId] = new( heartbeat.WorkerId, heartbeat.Status, heartbeat.ActiveJobs, @@ -61,7 +61,7 @@ public IReadOnlyCollection GetAll() now ); - return new WorkerStatusChangedEvent(heartbeat.WorkerId, null, heartbeat.Status); + return new(heartbeat.WorkerId, null, heartbeat.Status); } var old = existing.Status; @@ -74,14 +74,14 @@ public IReadOnlyCollection GetAll() }; return old == WorkerStatusType.Offline && heartbeat.Status != WorkerStatusType.Offline - ? new WorkerStatusChangedEvent(heartbeat.WorkerId, old, heartbeat.Status) - : null; + ? new WorkerStatusChangedEvent(heartbeat.WorkerId, old, heartbeat.Status) + : null; } } /// - /// Marks workers Offline whose last heartbeat is older than the configured timeout relative to - /// . Returns the resulting transitions. + /// Marks workers Offline whose last heartbeat is older than the configured timeout relative to + /// . Returns the resulting transitions. /// public IReadOnlyList Sweep(DateTime nowUtc) { @@ -97,7 +97,7 @@ public IReadOnlyList Sweep(DateTime nowUtc) } _workers[id] = info with { Status = WorkerStatusType.Offline }; - changes.Add(new WorkerStatusChangedEvent(id, info.Status, WorkerStatusType.Offline)); + changes.Add(new(id, info.Status, WorkerStatusType.Offline)); } } diff --git a/src/SquidStd.Workers/Attributes/RegisterJobHandlerAttribute.cs b/src/SquidStd.Workers/Attributes/RegisterJobHandlerAttribute.cs index 71b75a66..0babcfb7 100644 --- a/src/SquidStd.Workers/Attributes/RegisterJobHandlerAttribute.cs +++ b/src/SquidStd.Workers/Attributes/RegisterJobHandlerAttribute.cs @@ -1,9 +1,7 @@ namespace SquidStd.Workers.Attributes; /// -/// Marks a worker job handler for generated registration. +/// Marks a worker job handler for generated registration. /// [AttributeUsage(AttributeTargets.Class, Inherited = false)] -public sealed class RegisterJobHandlerAttribute : Attribute -{ -} +public sealed class RegisterJobHandlerAttribute : Attribute { } diff --git a/src/SquidStd.Workers/Data/Config/WorkersConfig.cs b/src/SquidStd.Workers/Data/Config/WorkersConfig.cs index 3340c181..d9bf2b4f 100644 --- a/src/SquidStd.Workers/Data/Config/WorkersConfig.cs +++ b/src/SquidStd.Workers/Data/Config/WorkersConfig.cs @@ -3,13 +3,13 @@ namespace SquidStd.Workers.Data.Config; /// -/// Configuration for the worker runtime (config section "workers"). +/// Configuration for the worker runtime (config section "workers"). /// public sealed class WorkersConfig { /// - /// Stable worker identity. When blank, the runtime falls back to the machine name - /// (in Docker, the container hostname). + /// Stable worker identity. When blank, the runtime falls back to the machine name + /// (in Docker, the container hostname). /// public string WorkerId { get; set; } = string.Empty; diff --git a/src/SquidStd.Workers/Exceptions/JobHandlerNotFoundException.cs b/src/SquidStd.Workers/Exceptions/JobHandlerNotFoundException.cs index 429efa2c..61a4d92d 100644 --- a/src/SquidStd.Workers/Exceptions/JobHandlerNotFoundException.cs +++ b/src/SquidStd.Workers/Exceptions/JobHandlerNotFoundException.cs @@ -1,7 +1,7 @@ namespace SquidStd.Workers.Exceptions; /// -/// Thrown when a job arrives whose name has no registered . +/// Thrown when a job arrives whose name has no registered . /// public sealed class JobHandlerNotFoundException : Exception { diff --git a/src/SquidStd.Workers/Extensions/WorkersRegistrationExtensions.cs b/src/SquidStd.Workers/Extensions/WorkersRegistrationExtensions.cs index 25dbae16..263b0723 100644 --- a/src/SquidStd.Workers/Extensions/WorkersRegistrationExtensions.cs +++ b/src/SquidStd.Workers/Extensions/WorkersRegistrationExtensions.cs @@ -8,14 +8,14 @@ namespace SquidStd.Workers.Extensions; /// -/// DryIoc registration helpers for the worker runtime. +/// DryIoc registration helpers for the worker runtime. /// public static class WorkersRegistrationExtensions { extension(IContainer container) { /// - /// Registers a job handler so the dispatcher can route jobs to it by name. + /// Registers a job handler so the dispatcher can route jobs to it by name. /// public IContainer AddJobHandler() where THandler : class, IJobHandler @@ -28,8 +28,8 @@ public IContainer AddJobHandler() } /// - /// Registers the worker runtime: the "workers" config section, shared state, job dispatcher, and the - /// consumer + heartbeat lifecycle services. + /// Registers the worker runtime: the "workers" config section, shared state, job dispatcher, and the + /// consumer + heartbeat lifecycle services. /// public IContainer AddWorkers() { diff --git a/src/SquidStd.Workers/Interfaces/IJobDispatcher.cs b/src/SquidStd.Workers/Interfaces/IJobDispatcher.cs index 9c9a18f3..fdae86b5 100644 --- a/src/SquidStd.Workers/Interfaces/IJobDispatcher.cs +++ b/src/SquidStd.Workers/Interfaces/IJobDispatcher.cs @@ -3,12 +3,12 @@ namespace SquidStd.Workers.Interfaces; /// -/// Routes a to the registered for its job name. +/// Routes a to the registered for its job name. /// public interface IJobDispatcher { /// - /// Dispatches the job to its handler. + /// Dispatches the job to its handler. /// /// No handler matches the job name. Task DispatchAsync(JobRequest job, CancellationToken cancellationToken); diff --git a/src/SquidStd.Workers/Interfaces/IJobHandler.cs b/src/SquidStd.Workers/Interfaces/IJobHandler.cs index efd2a910..d64a706e 100644 --- a/src/SquidStd.Workers/Interfaces/IJobHandler.cs +++ b/src/SquidStd.Workers/Interfaces/IJobHandler.cs @@ -3,7 +3,7 @@ namespace SquidStd.Workers.Interfaces; /// -/// Handles jobs of a single named kind. Implemented by consumers of the worker library. +/// Handles jobs of a single named kind. Implemented by consumers of the worker library. /// public interface IJobHandler { diff --git a/src/SquidStd.Workers/Interfaces/IWorkerState.cs b/src/SquidStd.Workers/Interfaces/IWorkerState.cs index c27933a9..98e90069 100644 --- a/src/SquidStd.Workers/Interfaces/IWorkerState.cs +++ b/src/SquidStd.Workers/Interfaces/IWorkerState.cs @@ -3,7 +3,7 @@ namespace SquidStd.Workers.Interfaces; /// -/// Shared runtime state of a worker, read by the heartbeat service and mutated by the consumer. +/// Shared runtime state of a worker, read by the heartbeat service and mutated by the consumer. /// public interface IWorkerState { diff --git a/src/SquidStd.Workers/Services/JobDispatcher.cs b/src/SquidStd.Workers/Services/JobDispatcher.cs index f097823d..e0c739cd 100644 --- a/src/SquidStd.Workers/Services/JobDispatcher.cs +++ b/src/SquidStd.Workers/Services/JobDispatcher.cs @@ -6,7 +6,7 @@ namespace SquidStd.Workers.Services; /// -/// Default : indexes the registered handlers by job name. +/// Default : indexes the registered handlers by job name. /// public sealed class JobDispatcher : IJobDispatcher { diff --git a/src/SquidStd.Workers/Services/WorkerConsumerService.cs b/src/SquidStd.Workers/Services/WorkerConsumerService.cs index 1c9e0903..94a10254 100644 --- a/src/SquidStd.Workers/Services/WorkerConsumerService.cs +++ b/src/SquidStd.Workers/Services/WorkerConsumerService.cs @@ -10,8 +10,8 @@ namespace SquidStd.Workers.Services; /// -/// Subscribes to the jobs queue and dispatches each to its handler, -/// bounded by . +/// Subscribes to the jobs queue and dispatches each to its handler, +/// bounded by . /// public sealed class WorkerConsumerService : ISquidStdService, IQueueMessageListenerAsync { @@ -29,7 +29,7 @@ public WorkerConsumerService(IMessageQueue queue, IJobDispatcher dispatcher, IWo _queue = queue; _dispatcher = dispatcher; _state = state; - _semaphore = new SemaphoreSlim(state.MaxConcurrency); + _semaphore = new(state.MaxConcurrency); _queueName = string.IsNullOrWhiteSpace(config.JobQueue) ? WorkerChannels.JobQueue : config.JobQueue; } diff --git a/src/SquidStd.Workers/Services/WorkerHeartbeatService.cs b/src/SquidStd.Workers/Services/WorkerHeartbeatService.cs index c76ad93a..8cc3a933 100644 --- a/src/SquidStd.Workers/Services/WorkerHeartbeatService.cs +++ b/src/SquidStd.Workers/Services/WorkerHeartbeatService.cs @@ -9,8 +9,8 @@ namespace SquidStd.Workers.Services; /// -/// Publishes a on the heartbeat topic immediately on start and then once -/// per configured interval. +/// Publishes a on the heartbeat topic immediately on start and then once +/// per configured interval. /// public sealed class WorkerHeartbeatService : ISquidStdService { @@ -42,8 +42,8 @@ public WorkerHeartbeatService(IMessageTopic topic, IWorkerState state, WorkersCo _interval = TimeSpan.FromSeconds(seconds); _topicName = string.IsNullOrWhiteSpace(config.HeartbeatTopic) - ? WorkerChannels.HeartbeatTopic - : config.HeartbeatTopic; + ? WorkerChannels.HeartbeatTopic + : config.HeartbeatTopic; } /// diff --git a/src/SquidStd.Workers/Services/WorkerState.cs b/src/SquidStd.Workers/Services/WorkerState.cs index 07849b18..e42747db 100644 --- a/src/SquidStd.Workers/Services/WorkerState.cs +++ b/src/SquidStd.Workers/Services/WorkerState.cs @@ -5,8 +5,8 @@ namespace SquidStd.Workers.Services; /// -/// Default backed by an interlocked counter; resolves identity and -/// concurrency from at construction. +/// Default backed by an interlocked counter; resolves identity and +/// concurrency from at construction. /// public sealed class WorkerState : IWorkerState { @@ -32,13 +32,9 @@ public WorkerState(WorkersConfig config) /// public void JobFinished() - { - Interlocked.Decrement(ref _activeJobs); - } + => Interlocked.Decrement(ref _activeJobs); /// public void JobStarted() - { - Interlocked.Increment(ref _activeJobs); - } + => Interlocked.Increment(ref _activeJobs); } diff --git a/tests/SquidStd.Tests/Abstractions/ConfigRegistrationDataTests.cs b/tests/SquidStd.Tests/Abstractions/ConfigRegistrationDataTests.cs index df9a2af0..d55e3d23 100644 --- a/tests/SquidStd.Tests/Abstractions/ConfigRegistrationDataTests.cs +++ b/tests/SquidStd.Tests/Abstractions/ConfigRegistrationDataTests.cs @@ -8,7 +8,7 @@ public class ConfigRegistrationDataTests [Fact] public void CreateDefault_IncompatibleFactory_Throws() { - var entry = new ConfigRegistrationData("test", typeof(TestConfig), () => new object()); + var entry = new ConfigRegistrationData("test", typeof(TestConfig), () => new()); Assert.Throws(() => entry.CreateDefault()); } @@ -34,18 +34,15 @@ public void CreateDefault_UsesFactory() [Fact] public void Ctor_InvalidSectionName_Throws() - { - Assert.Throws(() => new ConfigRegistrationData( + => Assert.Throws( + () => new ConfigRegistrationData( string.Empty, typeof(TestConfig), () => new TestConfig() ) ); - } [Fact] public void Ctor_NullType_Throws() - { - Assert.Throws(() => new ConfigRegistrationData("test", null!, () => new TestConfig())); - } + => Assert.Throws(() => new ConfigRegistrationData("test", null!, () => new TestConfig())); } diff --git a/tests/SquidStd.Tests/Actors/ActorAskTests.cs b/tests/SquidStd.Tests/Actors/ActorAskTests.cs index 29a4db32..5fbbbbb4 100644 --- a/tests/SquidStd.Tests/Actors/ActorAskTests.cs +++ b/tests/SquidStd.Tests/Actors/ActorAskTests.cs @@ -34,7 +34,7 @@ public async Task AskAsync_ReturnsReplyFromHandler() await actor.TellAsync(new Append("x")); await actor.TellAsync(new Append("y")); - var log = await actor.AskAsync(new GetLog()); + var log = await actor.AskAsync(new()); Assert.Equal("x,y", log); } @@ -47,7 +47,7 @@ public async Task AskAsync_WhenTokenCancelled_Faults() await actor.TellAsync(new Hold(gate)); // occupy the consumer so the request waits using var cts = new CancellationTokenSource(); - var ask = actor.AskAsync(new GetLog(), cts.Token); + var ask = actor.AskAsync(new(), cts.Token); cts.Cancel(); await Assert.ThrowsAsync(() => ask); diff --git a/tests/SquidStd.Tests/Actors/ActorErrorTests.cs b/tests/SquidStd.Tests/Actors/ActorErrorTests.cs index d09edca5..3900af98 100644 --- a/tests/SquidStd.Tests/Actors/ActorErrorTests.cs +++ b/tests/SquidStd.Tests/Actors/ActorErrorTests.cs @@ -1,4 +1,3 @@ -using SquidStd.Actors.Data; using SquidStd.Actors.Types; using SquidStd.Tests.Actors.Support; @@ -15,7 +14,7 @@ public async Task Isolate_KeepsProcessingAfterHandlerThrows() await actor.TellAsync(new Boom()); await actor.TellAsync(new Append("b")); - var log = await actor.AskAsync(new GetLog()); + var log = await actor.AskAsync(new()); Assert.Equal("a,b", log); Assert.Contains("boom", actor.Errors); @@ -26,9 +25,10 @@ public async Task AskAsync_PropagatesHandlerException() { await using var actor = new ProbeActor(); - var ex = await Assert.ThrowsAsync(() => - actor.AskAsync(new FailingRequest()) - ); + var ex = await Assert.ThrowsAsync( + () => + actor.AskAsync(new()) + ); Assert.Equal("ask-boom", ex.Message); } @@ -36,14 +36,11 @@ public async Task AskAsync_PropagatesHandlerException() [Fact] public async Task StopOnError_StopsProcessingAfterThrow() { - await using var actor = new ProbeActor( - new ActorOptions { ErrorPolicy = ActorErrorPolicy.StopOnError } - ); + await using var actor = new ProbeActor(new() { ErrorPolicy = ActorErrorPolicy.StopOnError }); await actor.TellAsync(new Boom()); // faults the mailbox await Task.Delay(50); - await Assert.ThrowsAsync(() => actor.AskAsync(new GetLog()) - ); + await Assert.ThrowsAsync(() => actor.AskAsync(new())); } } diff --git a/tests/SquidStd.Tests/Actors/ActorEventBusTests.cs b/tests/SquidStd.Tests/Actors/ActorEventBusTests.cs index f94daab5..ad6a96e9 100644 --- a/tests/SquidStd.Tests/Actors/ActorEventBusTests.cs +++ b/tests/SquidStd.Tests/Actors/ActorEventBusTests.cs @@ -16,7 +16,7 @@ public async Task SubscribeToEventBus_MapsEventsIntoMailboxInOrder() await bus.PublishAsync(new PingEvent("x")); await bus.PublishAsync(new PingEvent("y")); - var log = await actor.AskAsync(new GetLog()); + var log = await actor.AskAsync(new()); Assert.Equal("x,y", log); } @@ -31,7 +31,7 @@ public async Task SubscribeToEventBus_DisposingSubscription_StopsDelivery() subscription.Dispose(); await bus.PublishAsync(new PingEvent("second")); - var log = await actor.AskAsync(new GetLog()); + var log = await actor.AskAsync(new()); Assert.Equal("first", log); } } diff --git a/tests/SquidStd.Tests/Actors/ActorLifecycleTests.cs b/tests/SquidStd.Tests/Actors/ActorLifecycleTests.cs index 677c72f4..f71b3fd3 100644 --- a/tests/SquidStd.Tests/Actors/ActorLifecycleTests.cs +++ b/tests/SquidStd.Tests/Actors/ActorLifecycleTests.cs @@ -1,4 +1,3 @@ -using SquidStd.Actors.Data; using SquidStd.Tests.Actors.Support; namespace SquidStd.Tests.Actors; @@ -10,11 +9,11 @@ public async Task DisposeAsync_DrainsQueuedMessages() { var gate = new TaskCompletionSource(); var actor = new ProbeActor(); - await actor.TellAsync(new Hold(gate)); // in-flight, blocks until released (ignores cancellation) - await actor.TellAsync(new Append("a")); // queued behind the hold - await actor.TellAsync(new Append("b")); // queued behind the hold - var logTask = actor.AskAsync(new GetLog()); // queued last - await Task.Delay(50); // let the request enqueue before dispose completes the mailbox + await actor.TellAsync(new Hold(gate)); // in-flight, blocks until released (ignores cancellation) + await actor.TellAsync(new Append("a")); // queued behind the hold + await actor.TellAsync(new Append("b")); // queued behind the hold + var logTask = actor.AskAsync(new()); // queued last + await Task.Delay(50); // let the request enqueue before dispose completes the mailbox var disposeTask = actor.DisposeAsync(); gate.SetResult(); // release the hold so the queue can drain @@ -29,9 +28,9 @@ public async Task DisposeAsync_DrainsQueuedMessages() public async Task DisposeAsync_DrainTimeout_FaultsOutstandingAsk() { var gate = new TaskCompletionSource(); - var actor = new ProbeActor(new ActorOptions { ShutdownDrainTimeout = TimeSpan.FromMilliseconds(50) }); - await actor.TellAsync(new Hold(gate)); // in-flight, never released and ignores cancellation - var ask = actor.AskAsync(new GetLog()); // queued behind, never reached + var actor = new ProbeActor(new() { ShutdownDrainTimeout = TimeSpan.FromMilliseconds(50) }); + await actor.TellAsync(new Hold(gate)); // in-flight, never released and ignores cancellation + var ask = actor.AskAsync(new()); // queued behind, never reached await Task.Delay(50); await actor.DisposeAsync(); // drain exceeds the budget; the still-pending request is faulted @@ -47,8 +46,7 @@ public async Task TellAsync_AfterDispose_Throws() var actor = new ProbeActor(); await actor.DisposeAsync(); - await Assert.ThrowsAsync(async () => await actor.TellAsync(new Append("x")) - ); + await Assert.ThrowsAsync(async () => await actor.TellAsync(new Append("x"))); } [Fact] diff --git a/tests/SquidStd.Tests/Actors/ActorOrderingTests.cs b/tests/SquidStd.Tests/Actors/ActorOrderingTests.cs index fdc4b5be..0ad9ec37 100644 --- a/tests/SquidStd.Tests/Actors/ActorOrderingTests.cs +++ b/tests/SquidStd.Tests/Actors/ActorOrderingTests.cs @@ -14,7 +14,7 @@ public async Task Messages_AreProcessedInFifoOrder() await actor.TellAsync(new Append(i.ToString())); } - var log = await actor.AskAsync(new GetLog()); + var log = await actor.AskAsync(new()); Assert.Equal(string.Join(",", Enumerable.Range(0, 100)), log); } diff --git a/tests/SquidStd.Tests/Actors/ActorOverflowTests.cs b/tests/SquidStd.Tests/Actors/ActorOverflowTests.cs index 9da1daa4..8baba268 100644 --- a/tests/SquidStd.Tests/Actors/ActorOverflowTests.cs +++ b/tests/SquidStd.Tests/Actors/ActorOverflowTests.cs @@ -1,4 +1,3 @@ -using SquidStd.Actors.Data; using SquidStd.Actors.Types; using SquidStd.Tests.Actors.Support; @@ -10,9 +9,7 @@ public class ActorOverflowTests public async Task Wait_BlocksUntilCapacityFrees() { var gate = new TaskCompletionSource(); - await using var actor = new ProbeActor( - new ActorOptions { Capacity = 2, OverflowPolicy = ActorOverflowPolicy.Wait } - ); + await using var actor = new ProbeActor(new() { Capacity = 2, OverflowPolicy = ActorOverflowPolicy.Wait }); await actor.TellAsync(new Hold(gate)); // occupies the consumer (slot 1) await actor.TellAsync(new Append("a")); // buffered (slot 2) -> full @@ -29,9 +26,7 @@ public async Task Wait_BlocksUntilCapacityFrees() public async Task DropNewest_ReturnsFalseWhenFull() { var gate = new TaskCompletionSource(); - await using var actor = new ProbeActor( - new ActorOptions { Capacity = 1, OverflowPolicy = ActorOverflowPolicy.DropNewest } - ); + await using var actor = new ProbeActor(new() { Capacity = 1, OverflowPolicy = ActorOverflowPolicy.DropNewest }); await actor.TellAsync(new Hold(gate)); // occupies the only slot @@ -44,16 +39,14 @@ public async Task DropNewest_ReturnsFalseWhenFull() [Fact] public async Task Unbounded_AcceptsEveryMessage() { - await using var actor = new ProbeActor( - new ActorOptions { OverflowPolicy = ActorOverflowPolicy.Unbounded } - ); + await using var actor = new ProbeActor(new() { OverflowPolicy = ActorOverflowPolicy.Unbounded }); for (var i = 0; i < 500; i++) { Assert.True(await actor.TellAsync(new Append(i.ToString()))); } - var log = await actor.AskAsync(new GetLog()); + var log = await actor.AskAsync(new()); Assert.Equal(500, log.Split(",").Length); } } diff --git a/tests/SquidStd.Tests/Actors/Support/ProbeActor.cs b/tests/SquidStd.Tests/Actors/Support/ProbeActor.cs index 07474013..434b72fd 100644 --- a/tests/SquidStd.Tests/Actors/Support/ProbeActor.cs +++ b/tests/SquidStd.Tests/Actors/Support/ProbeActor.cs @@ -12,9 +12,7 @@ public sealed class ProbeActor : Actor public List Errors { get; } = new(); public ProbeActor(ActorOptions? options = null) - : base(options) - { - } + : base(options) { } protected override async ValueTask ReceiveAsync(IProbeMessage message, CancellationToken cancellationToken) { @@ -22,17 +20,21 @@ protected override async ValueTask ReceiveAsync(IProbeMessage message, Cancellat { case Append append: _log.Add(append.Value); + break; case Boom: throw new InvalidOperationException("boom"); case Hold hold: await hold.Gate.Task; + break; case HoldUntilCancelled: await Task.Delay(Timeout.Infinite, cancellationToken); + break; case GetLog getLog: getLog.Reply(string.Join(",", _log)); + break; case FailingRequest: throw new InvalidOperationException("ask-boom"); diff --git a/tests/SquidStd.Tests/Actors/Support/ProbeMessages.cs b/tests/SquidStd.Tests/Actors/Support/ProbeMessages.cs index b421e2a2..9eab0e24 100644 --- a/tests/SquidStd.Tests/Actors/Support/ProbeMessages.cs +++ b/tests/SquidStd.Tests/Actors/Support/ProbeMessages.cs @@ -3,9 +3,7 @@ namespace SquidStd.Tests.Actors.Support; /// Marker interface for every message the accepts. -public interface IProbeMessage -{ -} +public interface IProbeMessage { } /// Appends a value to the actor's log. public sealed record Append(string Value) : IProbeMessage; diff --git a/tests/SquidStd.Tests/AspNetCore/FakeSquidStdBootstrap.cs b/tests/SquidStd.Tests/AspNetCore/FakeSquidStdBootstrap.cs index a2d78432..17489326 100644 --- a/tests/SquidStd.Tests/AspNetCore/FakeSquidStdBootstrap.cs +++ b/tests/SquidStd.Tests/AspNetCore/FakeSquidStdBootstrap.cs @@ -15,9 +15,7 @@ internal sealed class FakeSquidStdBootstrap : ISquidStdBootstrap public IContainer Container { get; } = new Container(); public ISquidStdBootstrap ConfigureService(Func configure) - { - return ConfigureServices(configure); - } + => ConfigureServices(configure); public ISquidStdBootstrap ConfigureServices(Func configure) { @@ -25,8 +23,8 @@ public ISquidStdBootstrap ConfigureServices(Func configu var configuredContainer = configure(Container); return ReferenceEquals(configuredContainer, Container) - ? this - : throw new InvalidOperationException("ConfigureServices must return the bootstrap container instance."); + ? this + : throw new InvalidOperationException("ConfigureServices must return the bootstrap container instance."); } public ValueTask DisposeAsync() @@ -37,14 +35,10 @@ public ValueTask DisposeAsync() } public TService Resolve() - { - return Container.Resolve(); - } + => Container.Resolve(); public async Task RunAsync(CancellationToken cancellationToken = default) - { - await StartAsync(cancellationToken); - } + => await StartAsync(cancellationToken); public ValueTask StartAsync(CancellationToken cancellationToken = default) { diff --git a/tests/SquidStd.Tests/AspNetCore/SquidStdAspNetCoreBuilderExtensionsTests.cs b/tests/SquidStd.Tests/AspNetCore/SquidStdAspNetCoreBuilderExtensionsTests.cs index 994698a1..c29c0a56 100644 --- a/tests/SquidStd.Tests/AspNetCore/SquidStdAspNetCoreBuilderExtensionsTests.cs +++ b/tests/SquidStd.Tests/AspNetCore/SquidStdAspNetCoreBuilderExtensionsTests.cs @@ -108,7 +108,8 @@ public void UseSquidStd_WhenContainerCallbackReturnsDifferentContainer_Throws() using var temp = new TempDirectory(); var builder = CreateBuilder(temp.Path); - var ex = Assert.Throws(() => builder.UseSquidStd( + var ex = Assert.Throws( + () => builder.UseSquidStd( options => options.ConfigName = "app", _ => new Container() ) diff --git a/tests/SquidStd.Tests/AspNetCore/SquidStdHealthCheckAdapterTests.cs b/tests/SquidStd.Tests/AspNetCore/SquidStdHealthCheckAdapterTests.cs index 53ee2055..069efe15 100644 --- a/tests/SquidStd.Tests/AspNetCore/SquidStdHealthCheckAdapterTests.cs +++ b/tests/SquidStd.Tests/AspNetCore/SquidStdHealthCheckAdapterTests.cs @@ -12,7 +12,7 @@ public async Task CheckHealthAsync_MapsHealthy() { var adapter = new SquidStdHealthCheckAdapter(new FakeHealthCheck("ok", SquidHealthResult.Healthy("all good"))); - var result = await adapter.CheckHealthAsync(new HealthCheckContext()); + var result = await adapter.CheckHealthAsync(new()); Assert.Equal(HealthStatus.Healthy, result.Status); Assert.Equal("all good", result.Description); @@ -24,7 +24,7 @@ public async Task CheckHealthAsync_MapsUnhealthy_WithDescriptionAndException() var ex = new InvalidOperationException("boom"); var adapter = new SquidStdHealthCheckAdapter(new FakeHealthCheck("bad", SquidHealthResult.Unhealthy("down", ex))); - var result = await adapter.CheckHealthAsync(new HealthCheckContext()); + var result = await adapter.CheckHealthAsync(new()); Assert.Equal(HealthStatus.Unhealthy, result.Status); Assert.Equal("down", result.Description); diff --git a/tests/SquidStd.Tests/Bootstrap/SquidStdBootstrapTests.cs b/tests/SquidStd.Tests/Bootstrap/SquidStdBootstrapTests.cs index caa9a1bb..f01952fb 100644 --- a/tests/SquidStd.Tests/Bootstrap/SquidStdBootstrapTests.cs +++ b/tests/SquidStd.Tests/Bootstrap/SquidStdBootstrapTests.cs @@ -19,7 +19,7 @@ public async Task Create_RegistersBootstrapAndDefaultServices() { using var temp = new TempDirectory(); await using var bootstrap = - SquidStdBootstrap.Create(new SquidStdOptions { ConfigName = "app", RootDirectory = temp.Path }); + SquidStdBootstrap.Create(new() { ConfigName = "app", RootDirectory = temp.Path }); var resolved = bootstrap.Resolve(); var configManager = bootstrap.Resolve(); @@ -36,7 +36,7 @@ public async Task Create_WithContainer_UsesProvidedContainer() var container = new Container(); await using var bootstrap = SquidStdBootstrap.Create( - new SquidStdOptions + new() { ConfigName = "app", RootDirectory = temp.Path @@ -58,15 +58,13 @@ public async Task DisposeAsync_WithProvidedContainer_DoesNotDisposeContainer() var container = new Container(); await using (SquidStdBootstrap.Create( - new SquidStdOptions + new() { ConfigName = "app", RootDirectory = temp.Path }, container - )) - { - } + )) { } container.RegisterInstance("still-open"); @@ -82,9 +80,10 @@ public async Task RunAsync_StartsUntilCancellationThenStops() using var cancellation = new CancellationTokenSource(); var state = new RunTrackedState(); await using var bootstrap = - SquidStdBootstrap.Create(new SquidStdOptions { ConfigName = "app", RootDirectory = temp.Path }); + SquidStdBootstrap.Create(new() { ConfigName = "app", RootDirectory = temp.Path }); - bootstrap.ConfigureService(container => + bootstrap.ConfigureService( + container => { container.RegisterInstance(state); container.Register(Reuse.Singleton); @@ -127,7 +126,7 @@ public async Task StartAsync_ConfiguresFileSinkFromLoggerOptions() """ ); await using var bootstrap = - SquidStdBootstrap.Create(new SquidStdOptions { ConfigName = "app", RootDirectory = temp.Path }); + SquidStdBootstrap.Create(new() { ConfigName = "app", RootDirectory = temp.Path }); await bootstrap.StartAsync(CancellationToken.None); await bootstrap.StopAsync(CancellationToken.None); @@ -153,9 +152,10 @@ public async Task StartAsync_LoadsConfigBeforeResolvingRegisteredServices() ); var events = new List(); await using var bootstrap = - SquidStdBootstrap.Create(new SquidStdOptions { ConfigName = "app", RootDirectory = temp.Path }); + SquidStdBootstrap.Create(new() { ConfigName = "app", RootDirectory = temp.Path }); - bootstrap.ConfigureServices(container => + bootstrap.ConfigureServices( + container => { container.RegisterInstance(events); container.Register(Reuse.Singleton); @@ -183,9 +183,10 @@ public async Task StartAsync_OrdersServicesByPriorityAndStopAsync_ReversesOrder( using var temp = new TempDirectory(); var events = new List(); await using var bootstrap = - SquidStdBootstrap.Create(new SquidStdOptions { ConfigName = "app", RootDirectory = temp.Path }); + SquidStdBootstrap.Create(new() { ConfigName = "app", RootDirectory = temp.Path }); - bootstrap.ConfigureServices(container => + bootstrap.ConfigureServices( + container => { container.RegisterInstance(events); container.Register(Reuse.Singleton); @@ -222,9 +223,7 @@ public ValueTask StartAsync(CancellationToken cancellationToken = default) } public ValueTask StopAsync(CancellationToken cancellationToken = default) - { - return ValueTask.CompletedTask; - } + => ValueTask.CompletedTask; } private sealed class EarlyTrackedService(List events) : ISquidStdService diff --git a/tests/SquidStd.Tests/Caching/CacheMetricsProviderTests.cs b/tests/SquidStd.Tests/Caching/CacheMetricsProviderTests.cs index 2448e664..fa35387f 100644 --- a/tests/SquidStd.Tests/Caching/CacheMetricsProviderTests.cs +++ b/tests/SquidStd.Tests/Caching/CacheMetricsProviderTests.cs @@ -17,9 +17,7 @@ public async Task CollectAsync_ReportsCountersAndHitRatio() var samples = await metrics.CollectAsync(); double Value(string name) - { - return samples.Single(s => s.Name == name).Value; - } + => samples.Single(s => s.Name == name).Value; Assert.Equal(2, Value("hits")); Assert.Equal(1, Value("misses")); @@ -30,7 +28,5 @@ double Value(string name) [Fact] public void ProviderName_IsCache() - { - Assert.Equal("cache", new CacheMetricsProvider().ProviderName); - } + => Assert.Equal("cache", new CacheMetricsProvider().ProviderName); } diff --git a/tests/SquidStd.Tests/Caching/CacheServiceTests.cs b/tests/SquidStd.Tests/Caching/CacheServiceTests.cs index 66fc9f05..aa490ce6 100644 --- a/tests/SquidStd.Tests/Caching/CacheServiceTests.cs +++ b/tests/SquidStd.Tests/Caching/CacheServiceTests.cs @@ -9,26 +9,24 @@ public class CacheServiceTests { [Fact] public async Task Get_Missing_ReturnsDefault() - { - Assert.Null(await NewService(new FakeCacheProvider()).GetAsync("absent")); - } + => Assert.Null(await NewService(new()).GetAsync("absent")); [Fact] public async Task GetOrSet_Hit_DoesNotInvokeFactory() { - var service = NewService(new FakeCacheProvider()); + var service = NewService(new()); await service.SetAsync("k", 7); var calls = 0; var value = await service.GetOrSetAsync( - "k", - _ => - { - calls++; + "k", + _ => + { + calls++; - return Task.FromResult(0); - } - ); + return Task.FromResult(0); + } + ); Assert.Equal(7, value); Assert.Equal(0, calls); @@ -42,14 +40,14 @@ public async Task GetOrSet_Miss_InvokesFactoryAndStores() var calls = 0; var value = await service.GetOrSetAsync( - "k", - _ => - { - calls++; + "k", + _ => + { + calls++; - return Task.FromResult(99); - } - ); + return Task.FromResult(99); + } + ); Assert.Equal(99, value); Assert.Equal(1, calls); @@ -60,7 +58,7 @@ public async Task GetOrSet_Miss_InvokesFactoryAndStores() public async Task KeyPrefix_IsAppliedToProvider() { var provider = new FakeCacheProvider(); - var service = NewService(provider, new CacheOptions { KeyPrefix = "app:" }); + var service = NewService(provider, new() { KeyPrefix = "app:" }); await service.SetAsync("k", "v"); @@ -72,7 +70,7 @@ public async Task KeyPrefix_IsAppliedToProvider() public async Task Metrics_RecordHitAndMiss() { var metrics = new CacheMetricsProvider(); - var service = NewService(new FakeCacheProvider(), metrics: metrics); + var service = NewService(new(), metrics: metrics); await service.GetAsync("absent"); // miss await service.SetAsync("k", 1); @@ -87,7 +85,7 @@ public async Task Metrics_RecordHitAndMiss() public async Task Set_PerEntryTtl_OverridesDefault() { var provider = new FakeCacheProvider(); - var service = NewService(provider, new CacheOptions { DefaultTtl = TimeSpan.FromSeconds(30) }); + var service = NewService(provider, new() { DefaultTtl = TimeSpan.FromSeconds(30) }); await service.SetAsync("k", "v", TimeSpan.FromSeconds(5)); @@ -98,7 +96,7 @@ public async Task Set_PerEntryTtl_OverridesDefault() public async Task Set_UsesDefaultTtl_WhenNoneGiven() { var provider = new FakeCacheProvider(); - var service = NewService(provider, new CacheOptions { DefaultTtl = TimeSpan.FromSeconds(30) }); + var service = NewService(provider, new() { DefaultTtl = TimeSpan.FromSeconds(30) }); await service.SetAsync("k", "v"); @@ -108,7 +106,7 @@ public async Task Set_UsesDefaultTtl_WhenNoneGiven() [Fact] public async Task SetThenGet_RoundTrips() { - var service = NewService(new FakeCacheProvider()); + var service = NewService(new()); await service.SetAsync("k", 42); @@ -123,6 +121,6 @@ private static CacheService NewService( { var serializer = new JsonDataSerializer(); - return new CacheService(provider, serializer, serializer, options ?? new CacheOptions(), metrics); + return new(provider, serializer, serializer, options ?? new CacheOptions(), metrics); } } diff --git a/tests/SquidStd.Tests/Caching/InMemoryCacheProviderTests.cs b/tests/SquidStd.Tests/Caching/InMemoryCacheProviderTests.cs index 5fc13cfc..c803d1b3 100644 --- a/tests/SquidStd.Tests/Caching/InMemoryCacheProviderTests.cs +++ b/tests/SquidStd.Tests/Caching/InMemoryCacheProviderTests.cs @@ -20,9 +20,7 @@ public async Task Exists_And_Remove() [Fact] public async Task Get_Missing_ReturnsNull() - { - Assert.Null(await NewProvider().GetAsync("absent")); - } + => Assert.Null(await NewProvider().GetAsync("absent")); [Fact] public async Task SetThenGet_ReturnsValue() @@ -48,12 +46,8 @@ public async Task Ttl_Expires() } private static ReadOnlyMemory Bytes(string s) - { - return Encoding.UTF8.GetBytes(s); - } + => Encoding.UTF8.GetBytes(s); private static InMemoryCacheProvider NewProvider() - { - return new InMemoryCacheProvider(new MemoryCache(new MemoryCacheOptions())); - } + => new(new MemoryCache(new MemoryCacheOptions())); } diff --git a/tests/SquidStd.Tests/Caching/Redis/RedisCacheProviderTests.cs b/tests/SquidStd.Tests/Caching/Redis/RedisCacheProviderTests.cs index 42906a5c..e3e0b318 100644 --- a/tests/SquidStd.Tests/Caching/Redis/RedisCacheProviderTests.cs +++ b/tests/SquidStd.Tests/Caching/Redis/RedisCacheProviderTests.cs @@ -1,5 +1,4 @@ using System.Text; -using SquidStd.Caching.Redis.Data.Config; using SquidStd.Caching.Redis.Services; namespace SquidStd.Tests.Caching.Redis; @@ -57,22 +56,14 @@ public async Task Ttl_Expires() } private static ReadOnlyMemory Bytes(string s) - { - return Encoding.UTF8.GetBytes(s); - } + => Encoding.UTF8.GetBytes(s); private static string Key() - { - return "k-" + Guid.NewGuid().ToString("N"); - } + => "k-" + Guid.NewGuid().ToString("N"); private RedisCacheProvider NewProvider() - { - return new RedisCacheProvider(new RedisCacheOptions { Configuration = _fixture.ConnectionString }); - } + => new(new() { Configuration = _fixture.ConnectionString }); private static string Text(ReadOnlyMemory b) - { - return Encoding.UTF8.GetString(b.Span); - } + => Encoding.UTF8.GetString(b.Span); } diff --git a/tests/SquidStd.Tests/Caching/Redis/RedisContainerFixture.cs b/tests/SquidStd.Tests/Caching/Redis/RedisContainerFixture.cs index 8d7df788..bd17f84e 100644 --- a/tests/SquidStd.Tests/Caching/Redis/RedisContainerFixture.cs +++ b/tests/SquidStd.Tests/Caching/Redis/RedisContainerFixture.cs @@ -3,7 +3,7 @@ namespace SquidStd.Tests.Caching.Redis; /// -/// Starts a Redis container once for the whole collection and exposes its connection string. +/// Starts a Redis container once for the whole collection and exposes its connection string. /// public sealed class RedisContainerFixture : IAsyncLifetime { @@ -12,14 +12,10 @@ public sealed class RedisContainerFixture : IAsyncLifetime public string ConnectionString => _container.GetConnectionString(); public Task DisposeAsync() - { - return _container.DisposeAsync().AsTask(); - } + => _container.DisposeAsync().AsTask(); public Task InitializeAsync() - { - return _container.StartAsync(); - } + => _container.StartAsync(); } [CollectionDefinition(Name)] diff --git a/tests/SquidStd.Tests/Core/Extensions/Random/RandomExtensionsTests.cs b/tests/SquidStd.Tests/Core/Extensions/Random/RandomExtensionsTests.cs index a37472ed..cea8f810 100644 --- a/tests/SquidStd.Tests/Core/Extensions/Random/RandomExtensionsTests.cs +++ b/tests/SquidStd.Tests/Core/Extensions/Random/RandomExtensionsTests.cs @@ -42,9 +42,7 @@ public void RandomElement_ReturnsElementFromSource() [Fact] public void RandomElement_OnEmpty_ReturnsDefault() - { - Assert.Equal(0, Array.Empty().RandomElement()); - } + => Assert.Equal(0, Array.Empty().RandomElement()); [Fact] public void RandomElement_OnList_ReturnsElementFromSource() diff --git a/tests/SquidStd.Tests/Core/Extensions/Strings/Base64ExtensionsTests.cs b/tests/SquidStd.Tests/Core/Extensions/Strings/Base64ExtensionsTests.cs index f704254b..df6a32f2 100644 --- a/tests/SquidStd.Tests/Core/Extensions/Strings/Base64ExtensionsTests.cs +++ b/tests/SquidStd.Tests/Core/Extensions/Strings/Base64ExtensionsTests.cs @@ -31,13 +31,7 @@ public void FromBase64ToByteArray_DecodesBytes() Assert.Equal(bytes, encoded.FromBase64ToByteArray()); } - [Theory] - [InlineData("c3F1aWQ=", true)] - [InlineData("not base64!", false)] - [InlineData("abc", false)] - [InlineData("", false)] + [Theory, InlineData("c3F1aWQ=", true), InlineData("not base64!", false), InlineData("abc", false), InlineData("", false)] public void IsBase64String_DetectsValidPayloads(string value, bool expected) - { - Assert.Equal(expected, value.IsBase64String()); - } + => Assert.Equal(expected, value.IsBase64String()); } diff --git a/tests/SquidStd.Tests/Core/Pool/ObjectPoolTests.cs b/tests/SquidStd.Tests/Core/Pool/ObjectPoolTests.cs index 4305724b..6749d691 100644 --- a/tests/SquidStd.Tests/Core/Pool/ObjectPoolTests.cs +++ b/tests/SquidStd.Tests/Core/Pool/ObjectPoolTests.cs @@ -8,11 +8,12 @@ public class ObjectPoolTests public void Get_WhenEmpty_CreatesThroughFactory() { var created = 0; - using var pool = new ObjectPool(() => + using var pool = new ObjectPool( + () => { created++; - return new Boxed(); + return new(); } ); @@ -25,7 +26,7 @@ public void Get_WhenEmpty_CreatesThroughFactory() [Fact] public void Return_ThenGet_ReusesSameInstance() { - using var pool = new ObjectPool(() => new Boxed()); + using var pool = new ObjectPool(() => new()); var first = pool.Get(); pool.Return(first); @@ -38,7 +39,7 @@ public void Return_ThenGet_ReusesSameInstance() [Fact] public void Return_InvokesResetCallback() { - using var pool = new ObjectPool(() => new Boxed(), onReturn: boxed => boxed.Value = 0); + using var pool = new ObjectPool(() => new(), onReturn: boxed => boxed.Value = 0); var item = pool.Get(); item.Value = 42; @@ -50,7 +51,7 @@ public void Return_InvokesResetCallback() [Fact] public void Return_BeyondMaxRetained_DisposesInsteadOfPooling() { - using var pool = new ObjectPool(() => new Boxed(), 1); + using var pool = new ObjectPool(() => new(), 1); var kept = new Boxed(); var overflow = new Boxed(); @@ -65,7 +66,7 @@ public void Return_BeyondMaxRetained_DisposesInsteadOfPooling() [Fact] public void Dispose_DisposesRetainedInstances() { - var pool = new ObjectPool(() => new Boxed()); + var pool = new ObjectPool(() => new()); var item = pool.Get(); pool.Return(item); @@ -81,8 +82,6 @@ private sealed class Boxed : IDisposable public bool Disposed { get; private set; } public void Dispose() - { - Disposed = true; - } + => Disposed = true; } } diff --git a/tests/SquidStd.Tests/Core/Utils/ChecksumUtilsTests.cs b/tests/SquidStd.Tests/Core/Utils/ChecksumUtilsTests.cs index 047b3fe9..cf90385b 100644 --- a/tests/SquidStd.Tests/Core/Utils/ChecksumUtilsTests.cs +++ b/tests/SquidStd.Tests/Core/Utils/ChecksumUtilsTests.cs @@ -11,10 +11,9 @@ public void Compute_EmptyInput_ReturnsOffsetBasis() [Fact] public void Compute_KnownVector_MatchesFnv1a() - { + // FNV-1a 32-bit of ASCII "a" = 0xE40C292C. - Assert.Equal(0xE40C292Cu, ChecksumUtils.Compute("a"u8)); - } + => Assert.Equal(0xE40C292Cu, ChecksumUtils.Compute("a"u8)); [Fact] public void Compute_IsStableAcrossCalls() diff --git a/tests/SquidStd.Tests/Core/Utils/CryptoUtilsTests.cs b/tests/SquidStd.Tests/Core/Utils/CryptoUtilsTests.cs index beaeeab0..b879477d 100644 --- a/tests/SquidStd.Tests/Core/Utils/CryptoUtilsTests.cs +++ b/tests/SquidStd.Tests/Core/Utils/CryptoUtilsTests.cs @@ -27,10 +27,7 @@ public void Encrypt_ProducesDifferentPayloadsPerCall() Assert.NotEqual(first, second); } - [Theory] - [InlineData(16)] - [InlineData(24)] - [InlineData(32)] + [Theory, InlineData(16), InlineData(24), InlineData(32)] public void GenerateKey_ReturnsKeyOfRequestedSize(int size) { var key = Convert.FromBase64String(CryptoUtils.GenerateKey(size)); @@ -40,9 +37,7 @@ public void GenerateKey_ReturnsKeyOfRequestedSize(int size) [Fact] public void GenerateKey_WhenSizeInvalid_Throws() - { - Assert.Throws(() => CryptoUtils.GenerateKey(20)); - } + => Assert.Throws(() => CryptoUtils.GenerateKey(20)); [Fact] public void Decrypt_WithWrongKey_Throws() diff --git a/tests/SquidStd.Tests/Core/Utils/RngCollection.cs b/tests/SquidStd.Tests/Core/Utils/RngCollection.cs index c6fd733e..414525ba 100644 --- a/tests/SquidStd.Tests/Core/Utils/RngCollection.cs +++ b/tests/SquidStd.Tests/Core/Utils/RngCollection.cs @@ -1,10 +1,8 @@ namespace SquidStd.Tests.Core.Utils; /// -/// Serializes tests that exercise the global state so a -/// seeded sequence is not perturbed by concurrent consumers. +/// Serializes tests that exercise the global state so a +/// seeded sequence is not perturbed by concurrent consumers. /// [CollectionDefinition("BuiltInRng")] -public sealed class RngCollection -{ -} +public sealed class RngCollection { } diff --git a/tests/SquidStd.Tests/Core/Utils/SslUtilsTests.cs b/tests/SquidStd.Tests/Core/Utils/SslUtilsTests.cs index 0a1bd0ad..6c5d20cc 100644 --- a/tests/SquidStd.Tests/Core/Utils/SslUtilsTests.cs +++ b/tests/SquidStd.Tests/Core/Utils/SslUtilsTests.cs @@ -75,7 +75,5 @@ public void LoadFromPfx_LoadsCertificateWithPrivateKey() [Fact] public void LoadFromPem_WhenPathBlank_Throws() - { - Assert.Throws(() => SslUtils.LoadFromPem(" ")); - } + => Assert.Throws(() => SslUtils.LoadFromPem(" ")); } diff --git a/tests/SquidStd.Tests/Crypto/Pgp/AesGcmPgpKeyStoreTests.cs b/tests/SquidStd.Tests/Crypto/Pgp/AesGcmPgpKeyStoreTests.cs index d4e820db..6b8bb4c3 100644 --- a/tests/SquidStd.Tests/Crypto/Pgp/AesGcmPgpKeyStoreTests.cs +++ b/tests/SquidStd.Tests/Crypto/Pgp/AesGcmPgpKeyStoreTests.cs @@ -1,5 +1,4 @@ using System.Text; -using SquidStd.Core.Data.Storage; using SquidStd.Crypto.Pgp.Services; using SquidStd.Services.Core.Services.Storage; using SquidStd.Tests.Crypto.Pgp.Support; @@ -20,7 +19,7 @@ public AesGcmPgpKeyStoreTests(PgpTestKeys keys) public async Task SaveThenLoad_RoundTripsAndBlobIsNotPlaintext() { var path = Path.Combine(Path.GetTempPath(), "squidstd-pgp-" + Guid.NewGuid().ToString("N") + ".bin"); - var protector = new AesGcmSecretProtector(new SecretsConfig()); + var protector = new AesGcmSecretProtector(new()); try { diff --git a/tests/SquidStd.Tests/Crypto/Pgp/FilePgpKeyStoreTests.cs b/tests/SquidStd.Tests/Crypto/Pgp/FilePgpKeyStoreTests.cs index f905de9a..de8af47a 100644 --- a/tests/SquidStd.Tests/Crypto/Pgp/FilePgpKeyStoreTests.cs +++ b/tests/SquidStd.Tests/Crypto/Pgp/FilePgpKeyStoreTests.cs @@ -38,7 +38,7 @@ public async Task SaveThenLoad_RestoresPublicAndSecretKeys() { if (Directory.Exists(dir)) { - Directory.Delete(dir, recursive: true); + Directory.Delete(dir, true); } } } diff --git a/tests/SquidStd.Tests/Crypto/Pgp/PgpServiceEncryptionTests.cs b/tests/SquidStd.Tests/Crypto/Pgp/PgpServiceEncryptionTests.cs index 0a1904c8..d48d5304 100644 --- a/tests/SquidStd.Tests/Crypto/Pgp/PgpServiceEncryptionTests.cs +++ b/tests/SquidStd.Tests/Crypto/Pgp/PgpServiceEncryptionTests.cs @@ -35,7 +35,8 @@ public async Task EncryptFor_UnknownRecipient_ThrowsKeyNotFound() { var service = new PgpService(new PgpKeyring()); - await Assert.ThrowsAsync(() => service.EncryptForAsync( + await Assert.ThrowsAsync( + () => service.EncryptForAsync( "nobody@squidstd.test", Encoding.UTF8.GetBytes("x") ) diff --git a/tests/SquidStd.Tests/Crypto/Pgp/PgpServiceSigningTests.cs b/tests/SquidStd.Tests/Crypto/Pgp/PgpServiceSigningTests.cs index cad70f8d..f3554f36 100644 --- a/tests/SquidStd.Tests/Crypto/Pgp/PgpServiceSigningTests.cs +++ b/tests/SquidStd.Tests/Crypto/Pgp/PgpServiceSigningTests.cs @@ -37,10 +37,10 @@ public async Task Verify_TamperedMessage_IsInvalid() var service = new PgpService(keyring); var signed = await service.SignAsync( - Encoding.UTF8.GetBytes("original"), - PgpTestKeys.AliceIdentity, - PgpTestKeys.AlicePassphrase - ); + Encoding.UTF8.GetBytes("original"), + PgpTestKeys.AliceIdentity, + PgpTestKeys.AlicePassphrase + ); var tampered = MutateBody(signed); var result = await service.VerifyAsync(tampered); @@ -58,11 +58,11 @@ public async Task EncryptAndSign_ThenDecryptAndVerify_RoundTripsAndValidates() var payload = Encoding.UTF8.GetBytes("confidential and signed"); var armored = await service.EncryptAndSignForAsync( - PgpTestKeys.AliceIdentity, - payload, - PgpTestKeys.BobIdentity, - PgpTestKeys.BobPassphrase - ); + PgpTestKeys.AliceIdentity, + payload, + PgpTestKeys.BobIdentity, + PgpTestKeys.BobPassphrase + ); var result = await service.DecryptAndVerifyAsync(armored, PgpTestKeys.AlicePassphrase); Assert.Equal(payload, result.Data); @@ -80,11 +80,11 @@ public async Task DecryptAndVerify_SignerNotInKeyring_RecoversDataButInvalid() var signingService = new PgpService(signingKeyring); var payload = Encoding.UTF8.GetBytes("signed by an unknown party"); var armored = await signingService.EncryptAndSignForAsync( - PgpTestKeys.AliceIdentity, - payload, - PgpTestKeys.BobIdentity, - PgpTestKeys.BobPassphrase - ); + PgpTestKeys.AliceIdentity, + payload, + PgpTestKeys.BobIdentity, + PgpTestKeys.BobPassphrase + ); var recipientOnly = new PgpKeyring(); recipientOnly.Import(_keys.AlicePrivate); // no Bob public key @@ -107,7 +107,7 @@ private static string MutateBody(string signed) { var chars = lines[mid].ToCharArray(); chars[2] = chars[2] == 'A' ? 'B' : 'A'; - lines[mid] = new string(chars); + lines[mid] = new(chars); } return string.Join('\n', lines); diff --git a/tests/SquidStd.Tests/Crypto/Pgp/Support/PgpKeysCollection.cs b/tests/SquidStd.Tests/Crypto/Pgp/Support/PgpKeysCollection.cs index 93185bd8..1529fdde 100644 --- a/tests/SquidStd.Tests/Crypto/Pgp/Support/PgpKeysCollection.cs +++ b/tests/SquidStd.Tests/Crypto/Pgp/Support/PgpKeysCollection.cs @@ -2,6 +2,4 @@ namespace SquidStd.Tests.Crypto.Pgp.Support; /// Shares one instance across the PGP test classes. [CollectionDefinition("PgpKeys")] -public sealed class PgpKeysCollection : ICollectionFixture -{ -} +public sealed class PgpKeysCollection : ICollectionFixture { } diff --git a/tests/SquidStd.Tests/Crypto/Pgp/Support/PgpTestKeys.cs b/tests/SquidStd.Tests/Crypto/Pgp/Support/PgpTestKeys.cs index 16b6e9ce..e87d29bf 100644 --- a/tests/SquidStd.Tests/Crypto/Pgp/Support/PgpTestKeys.cs +++ b/tests/SquidStd.Tests/Crypto/Pgp/Support/PgpTestKeys.cs @@ -1,10 +1,11 @@ +using System.Text; using PgpCore; namespace SquidStd.Tests.Crypto.Pgp.Support; /// -/// Generates two throwaway armored RSA-2048 key pairs once for the whole assembly. Key generation is -/// expensive, so tests share this fixture via . +/// Generates two throwaway armored RSA-2048 key pairs once for the whole assembly. Key generation is +/// expensive, so tests share this fixture via . /// public sealed class PgpTestKeys { @@ -38,7 +39,5 @@ private static (string Public, string Private) Generate(string identity, string } private static string ReadAll(MemoryStream stream) - { - return System.Text.Encoding.UTF8.GetString(stream.ToArray()); - } + => Encoding.UTF8.GetString(stream.ToArray()); } diff --git a/tests/SquidStd.Tests/Crypto/Vfs/CryptoFileSystemDiskTests.cs b/tests/SquidStd.Tests/Crypto/Vfs/CryptoFileSystemDiskTests.cs index b92cc5b5..e7f832d2 100644 --- a/tests/SquidStd.Tests/Crypto/Vfs/CryptoFileSystemDiskTests.cs +++ b/tests/SquidStd.Tests/Crypto/Vfs/CryptoFileSystemDiskTests.cs @@ -8,9 +8,7 @@ namespace SquidStd.Tests.Crypto.Vfs; public class CryptoFileSystemDiskTests { private static CryptoVaultOptions FastOptions() - { - return new CryptoVaultOptions { Argon2MemoryKib = 8192, Argon2Iterations = 1 }; - } + => new() { Argon2MemoryKib = 8192, Argon2Iterations = 1 }; [Fact] public async Task Lock_AfterWrites_OverZipBackend_DoesNotThrow() diff --git a/tests/SquidStd.Tests/Crypto/Vfs/CryptoFileSystemLockTests.cs b/tests/SquidStd.Tests/Crypto/Vfs/CryptoFileSystemLockTests.cs index 94353dfb..87db65ef 100644 --- a/tests/SquidStd.Tests/Crypto/Vfs/CryptoFileSystemLockTests.cs +++ b/tests/SquidStd.Tests/Crypto/Vfs/CryptoFileSystemLockTests.cs @@ -8,9 +8,7 @@ namespace SquidStd.Tests.Crypto.Vfs; public class CryptoFileSystemLockTests { private static CryptoVaultOptions FastOptions() - { - return new CryptoVaultOptions { Argon2MemoryKib = 8192, Argon2Iterations = 1 }; - } + => new() { Argon2MemoryKib = 8192, Argon2Iterations = 1 }; [Fact] public async Task LockedOperations_Throw_UntilUnlocked() @@ -18,8 +16,7 @@ public async Task LockedOperations_Throw_UntilUnlocked() var vault = new CryptoFileSystem(new InMemoryFileSystem(), FastOptions()); Assert.False(vault.IsUnlocked); - await Assert.ThrowsAsync(() => vault.WriteAllBytesAsync("a", new byte[] { 1 }).AsTask() - ); + await Assert.ThrowsAsync(() => vault.WriteAllBytesAsync("a", new byte[] { 1 }).AsTask()); vault.Unlock("pw"); Assert.True(vault.IsUnlocked); diff --git a/tests/SquidStd.Tests/Crypto/Vfs/CryptoFileSystemTests.cs b/tests/SquidStd.Tests/Crypto/Vfs/CryptoFileSystemTests.cs index 394594c6..83f1f436 100644 --- a/tests/SquidStd.Tests/Crypto/Vfs/CryptoFileSystemTests.cs +++ b/tests/SquidStd.Tests/Crypto/Vfs/CryptoFileSystemTests.cs @@ -9,9 +9,7 @@ namespace SquidStd.Tests.Crypto.Vfs; public class CryptoFileSystemTests { private static CryptoVaultOptions FastOptions() - { - return new CryptoVaultOptions { Argon2MemoryKib = 8192, Argon2Iterations = 1 }; - } + => new() { Argon2MemoryKib = 8192, Argon2Iterations = 1 }; [Fact] public async Task Write_Read_List_Delete_RoundTrips() @@ -23,6 +21,7 @@ public async Task Write_Read_List_Delete_RoundTrips() Assert.Equal("hello", Encoding.UTF8.GetString((await vault.ReadAllBytesAsync("docs/cv.pdf"))!)); var paths = new List(); + await foreach (var e in vault.ListAsync()) { paths.Add(e.Path); diff --git a/tests/SquidStd.Tests/Crypto/Vfs/EntryCipherTests.cs b/tests/SquidStd.Tests/Crypto/Vfs/EntryCipherTests.cs index 4a10faa1..418e31b6 100644 --- a/tests/SquidStd.Tests/Crypto/Vfs/EntryCipherTests.cs +++ b/tests/SquidStd.Tests/Crypto/Vfs/EntryCipherTests.cs @@ -13,7 +13,7 @@ public async Task EncryptThenDecrypt_RoundTripsAcrossChunkBoundaries() var key = RandomNumberGenerator.GetBytes(32); var payload = RandomNumberGenerator.GetBytes(200_000); // > 3 chunks at 64 KiB - using var cipher = new EntryCipher(key, chunkSize: 65536); + using var cipher = new EntryCipher(key, 65536); using var encrypted = new MemoryStream(); await cipher.EncryptAsync(new MemoryStream(payload), encrypted); @@ -34,7 +34,8 @@ public async Task Decrypt_WrongKey_Throws() using var wrong = new EntryCipher(RandomNumberGenerator.GetBytes(32), 65536); encrypted.Position = 0; - await Assert.ThrowsAsync(() => wrong.DecryptAsync(encrypted, new MemoryStream()) + await Assert.ThrowsAsync( + () => wrong.DecryptAsync(encrypted, new MemoryStream()) ); } @@ -49,7 +50,8 @@ public async Task Decrypt_RecordLengthExceedingChunkSize_Throws_WithoutAllocatin BinaryPrimitives.WriteInt32BigEndian(header, chunkSize + 1); using var cipher = new EntryCipher(RandomNumberGenerator.GetBytes(32), chunkSize); - await Assert.ThrowsAsync(() => cipher.DecryptAsync(new MemoryStream(header), new MemoryStream()) + await Assert.ThrowsAsync( + () => cipher.DecryptAsync(new MemoryStream(header), new MemoryStream()) ); } } diff --git a/tests/SquidStd.Tests/Crypto/Vfs/VaultHeaderTests.cs b/tests/SquidStd.Tests/Crypto/Vfs/VaultHeaderTests.cs index 93ad01c0..fdf0b083 100644 --- a/tests/SquidStd.Tests/Crypto/Vfs/VaultHeaderTests.cs +++ b/tests/SquidStd.Tests/Crypto/Vfs/VaultHeaderTests.cs @@ -11,10 +11,10 @@ public void Serialize_Parse_RoundTrips() "SQVFS1", 1, [1, 2, 3, 4], - MemoryKib: 8192, - Iterations: 2, - Parallelism: 1, - ChunkSize: 65536 + 8192, + 2, + 1, + 65536 ); var parsed = VaultHeader.Parse(header.Serialize()); diff --git a/tests/SquidStd.Tests/Crypto/Vfs/VaultIndexTests.cs b/tests/SquidStd.Tests/Crypto/Vfs/VaultIndexTests.cs index a7f0efd9..4ca031ee 100644 --- a/tests/SquidStd.Tests/Crypto/Vfs/VaultIndexTests.cs +++ b/tests/SquidStd.Tests/Crypto/Vfs/VaultIndexTests.cs @@ -8,7 +8,7 @@ public class VaultIndexTests public void Serialize_Parse_RoundTripsEntries() { var index = new VaultIndex(); - index.Set("docs/cv.pdf", new VaultIndexEntry("a1b2", 100, DateTimeOffset.UnixEpoch)); + index.Set("docs/cv.pdf", new("a1b2", 100, DateTimeOffset.UnixEpoch)); var parsed = VaultIndex.Parse(index.Serialize()); diff --git a/tests/SquidStd.Tests/Database/ConnectionStringParserTests.cs b/tests/SquidStd.Tests/Database/ConnectionStringParserTests.cs index f5d25619..73ee8d68 100644 --- a/tests/SquidStd.Tests/Database/ConnectionStringParserTests.cs +++ b/tests/SquidStd.Tests/Database/ConnectionStringParserTests.cs @@ -7,9 +7,7 @@ public class ConnectionStringParserTests { [Fact] public void Parse_MissingHostForServerProvider_Throws() - { - Assert.Throws(() => ConnectionStringParser.Parse("postgres:///db")); - } + => Assert.Throws(() => ConnectionStringParser.Parse("postgres:///db")); [Fact] public void Parse_MySql_BuildsNativeString() @@ -78,7 +76,5 @@ public void Parse_SqlServer_BuildsNativeString() [Fact] public void Parse_UnknownScheme_Throws() - { - Assert.Throws(() => ConnectionStringParser.Parse("oracle://h/db")); - } + => Assert.Throws(() => ConnectionStringParser.Parse("oracle://h/db")); } diff --git a/tests/SquidStd.Tests/Database/FreeSqlDataAccessTests.cs b/tests/SquidStd.Tests/Database/FreeSqlDataAccessTests.cs index 42846655..4ecec4bd 100644 --- a/tests/SquidStd.Tests/Database/FreeSqlDataAccessTests.cs +++ b/tests/SquidStd.Tests/Database/FreeSqlDataAccessTests.cs @@ -1,4 +1,3 @@ -using SquidStd.Database.Abstractions.Data.Database; using SquidStd.Database.Abstractions.Data.Entities; using SquidStd.Database.Data; using SquidStd.Database.Services; @@ -29,8 +28,8 @@ public async Task DisposeAsync() public async Task InitializeAsync() { _dbPath = Path.Combine(Path.GetTempPath(), "squidstd-db-" + Guid.NewGuid().ToString("N") + ".db"); - _service = new DatabaseService( - new DatabaseConfig + _service = new( + new() { ConnectionString = $"sqlite://{_dbPath}", AutoMigrate = true @@ -84,7 +83,7 @@ await access.BulkInsertAsync( public async Task DeleteAsync_RemovesRow() { var access = NewAccess(); - var user = await access.InsertAsync(new SampleUser { Name = "Cara", Age = 40 }); + var user = await access.InsertAsync(new() { Name = "Cara", Age = 40 }); Assert.True(await access.DeleteAsync(user.Id)); Assert.Null(await access.GetByIdAsync(user.Id)); @@ -98,7 +97,7 @@ public async Task GetPagedAsync_ReturnsMetadata() for (var i = 0; i < 25; i++) { - await access.InsertAsync(new SampleUser { Name = $"u{i}", Age = i }); + await access.InsertAsync(new() { Name = $"u{i}", Age = i }); } var page = await access.GetPagedAsync(2, 10, orderBy: u => u.Age); @@ -115,7 +114,7 @@ public async Task GetPagedAsync_ReturnsMetadata() public async Task InsertAsync_RollsBackOnFailure() { var access = NewAccess(); - var first = await access.InsertAsync(new SampleUser { Name = "first", Age = 1 }); + var first = await access.InsertAsync(new() { Name = "first", Age = 1 }); // Re-using an existing primary key forces a unique-constraint violation. var duplicate = new SampleUser { Id = first.Id, Name = "dup", Age = 2 }; @@ -128,7 +127,7 @@ public async Task InsertAsync_RollsBackOnFailure() public async Task InsertAsync_SetsIdAndTimestamps() { var access = NewAccess(); - var inserted = await access.InsertAsync(new SampleUser { Name = "Ann", Age = 30 }); + var inserted = await access.InsertAsync(new() { Name = "Ann", Age = 30 }); Assert.NotEqual(Guid.Empty, inserted.Id); Assert.NotEqual(default, inserted.Created); @@ -143,7 +142,7 @@ public async Task InsertAsync_SetsIdAndTimestamps() public async Task UpdateAsync_BumpsUpdated() { var access = NewAccess(); - var user = await access.InsertAsync(new SampleUser { Name = "Bob", Age = 20 }); + var user = await access.InsertAsync(new() { Name = "Bob", Age = 20 }); user.Age = 21; var updated = await access.UpdateAsync(user); @@ -153,7 +152,5 @@ public async Task UpdateAsync_BumpsUpdated() } private FreeSqlDataAccess NewAccess() - { - return new FreeSqlDataAccess(_service); - } + => new(_service); } diff --git a/tests/SquidStd.Tests/Env/ReplaceEnvTests.cs b/tests/SquidStd.Tests/Env/ReplaceEnvTests.cs index 59f97402..84180983 100644 --- a/tests/SquidStd.Tests/Env/ReplaceEnvTests.cs +++ b/tests/SquidStd.Tests/Env/ReplaceEnvTests.cs @@ -11,14 +11,9 @@ public void ReplaceEnv_LeavesUnknownVariableUntouched() Assert.Equal("x=$SQUID_MISSING_VAR", "x=$SQUID_MISSING_VAR".ReplaceEnv()); } - [Theory] - [InlineData(null)] - [InlineData("")] - [InlineData("no tokens here")] + [Theory, InlineData(null), InlineData(""), InlineData("no tokens here")] public void ReplaceEnv_PassesThroughWhenNothingToReplace(string? input) - { - Assert.Equal(input, input!.ReplaceEnv()); - } + => Assert.Equal(input, input!.ReplaceEnv()); [Fact] public void ReplaceEnv_SubstitutesKnownVariable() diff --git a/tests/SquidStd.Tests/Extensions/Directories/DirectoriesExtensionTests.cs b/tests/SquidStd.Tests/Extensions/Directories/DirectoriesExtensionTests.cs index b9cd69dd..7cd656a5 100644 --- a/tests/SquidStd.Tests/Extensions/Directories/DirectoriesExtensionTests.cs +++ b/tests/SquidStd.Tests/Extensions/Directories/DirectoriesExtensionTests.cs @@ -4,14 +4,9 @@ namespace SquidStd.Tests.Extensions.Directories; public class DirectoriesExtensionTests { - [Theory] - [InlineData("")] - [InlineData(" ")] - [InlineData(null)] + [Theory, InlineData(""), InlineData(" "), InlineData(null)] public void ResolvePathAndEnvs_NullOrWhitespace_ReturnsNull(string? path) - { - Assert.Null(path!.ResolvePathAndEnvs()); - } + => Assert.Null(path!.ResolvePathAndEnvs()); [Fact] public void ResolvePathAndEnvs_TildePrefix_ExpandsToUserProfile() diff --git a/tests/SquidStd.Tests/Extensions/Env/EnvExtensionsTests.cs b/tests/SquidStd.Tests/Extensions/Env/EnvExtensionsTests.cs index 9a6e1e07..1a408dfc 100644 --- a/tests/SquidStd.Tests/Extensions/Env/EnvExtensionsTests.cs +++ b/tests/SquidStd.Tests/Extensions/Env/EnvExtensionsTests.cs @@ -6,13 +6,9 @@ public class EnvExtensionsTests { private const string TestVariable = "SQUIDSTD_UNIT_TEST_VAR"; - [Theory] - [InlineData("")] - [InlineData("no variables here")] + [Theory, InlineData(""), InlineData("no variables here")] public void ExpandEnvironmentVariables_NoMatch_ReturnsInput(string input) - { - Assert.Equal(input, input.ExpandEnvironmentVariables()); - } + => Assert.Equal(input, input.ExpandEnvironmentVariables()); [Fact] public void ExpandEnvironmentVariables_ReplacesDollarPrefixedVariable() diff --git a/tests/SquidStd.Tests/Extensions/Logger/LogLevelExtensionsTests.cs b/tests/SquidStd.Tests/Extensions/Logger/LogLevelExtensionsTests.cs index d229a467..bb72c066 100644 --- a/tests/SquidStd.Tests/Extensions/Logger/LogLevelExtensionsTests.cs +++ b/tests/SquidStd.Tests/Extensions/Logger/LogLevelExtensionsTests.cs @@ -6,21 +6,14 @@ namespace SquidStd.Tests.Extensions.Logger; public class LogLevelExtensionsTests { - [Theory] - [InlineData(LogLevelType.Trace, LogEventLevel.Verbose)] - [InlineData(LogLevelType.Debug, LogEventLevel.Debug)] - [InlineData(LogLevelType.Information, LogEventLevel.Information)] - [InlineData(LogLevelType.Warning, LogEventLevel.Warning)] - [InlineData(LogLevelType.Error, LogEventLevel.Error)] - [InlineData(LogLevelType.Critical, LogEventLevel.Fatal)] + [Theory, InlineData(LogLevelType.Trace, LogEventLevel.Verbose), InlineData(LogLevelType.Debug, LogEventLevel.Debug), + InlineData(LogLevelType.Information, LogEventLevel.Information), + InlineData(LogLevelType.Warning, LogEventLevel.Warning), InlineData(LogLevelType.Error, LogEventLevel.Error), + InlineData(LogLevelType.Critical, LogEventLevel.Fatal)] public void ToSerilogLogLevel_KnownLevels_MapsExpected(LogLevelType input, LogEventLevel expected) - { - Assert.Equal(expected, input.ToSerilogLogLevel()); - } + => Assert.Equal(expected, input.ToSerilogLogLevel()); [Fact] public void ToSerilogLogLevel_UnmappedLevels_FallsBackToInformation() - { - Assert.Equal(LogEventLevel.Information, ((LogLevelType)255).ToSerilogLogLevel()); - } + => Assert.Equal(LogEventLevel.Information, ((LogLevelType)255).ToSerilogLogLevel()); } diff --git a/tests/SquidStd.Tests/Extensions/Strings/StringMethodExtensionTests.cs b/tests/SquidStd.Tests/Extensions/Strings/StringMethodExtensionTests.cs index 53ea0b58..9aee1867 100644 --- a/tests/SquidStd.Tests/Extensions/Strings/StringMethodExtensionTests.cs +++ b/tests/SquidStd.Tests/Extensions/Strings/StringMethodExtensionTests.cs @@ -6,61 +6,41 @@ public class StringMethodExtensionTests { [Fact] public void ToCamelCase_DelegatesToStringUtils() - { - Assert.Equal("helloWorld", "HelloWorld".ToCamelCase()); - } + => Assert.Equal("helloWorld", "HelloWorld".ToCamelCase()); [Fact] public void ToDotCase_DelegatesToStringUtils() - { - Assert.Equal("hello.world", "HelloWorld".ToDotCase()); - } + => Assert.Equal("hello.world", "HelloWorld".ToDotCase()); [Fact] public void ToKebabCase_DelegatesToStringUtils() - { - Assert.Equal("hello-world", "HelloWorld".ToKebabCase()); - } + => Assert.Equal("hello-world", "HelloWorld".ToKebabCase()); [Fact] public void ToPascalCase_DelegatesToStringUtils() - { - Assert.Equal("HelloWorld", "hello_world".ToPascalCase()); - } + => Assert.Equal("HelloWorld", "hello_world".ToPascalCase()); [Fact] public void ToPathCase_DelegatesToStringUtils() - { - Assert.Equal("hello/world", "HelloWorld".ToPathCase()); - } + => Assert.Equal("hello/world", "HelloWorld".ToPathCase()); [Fact] public void ToSentenceCase_DelegatesToStringUtils() - { - Assert.Equal("Hello world", "hello world".ToSentenceCase()); - } + => Assert.Equal("Hello world", "hello world".ToSentenceCase()); [Fact] public void ToSnakeCase_DelegatesToStringUtils() - { - Assert.Equal("hello_world", "HelloWorld".ToSnakeCase()); - } + => Assert.Equal("hello_world", "HelloWorld".ToSnakeCase()); [Fact] public void ToSnakeCaseUpper_DelegatesToStringUtils() - { - Assert.Equal("HELLO_WORLD", "HelloWorld".ToSnakeCaseUpper()); - } + => Assert.Equal("HELLO_WORLD", "HelloWorld".ToSnakeCaseUpper()); [Fact] public void ToTitleCase_DelegatesToStringUtils() - { - Assert.Equal("Hello World", "hello_world".ToTitleCase()); - } + => Assert.Equal("Hello World", "hello_world".ToTitleCase()); [Fact] public void ToTrainCase_DelegatesToStringUtils() - { - Assert.Equal("Hello-World", "hello_world".ToTrainCase()); - } + => Assert.Equal("Hello-World", "hello_world".ToTrainCase()); } diff --git a/tests/SquidStd.Tests/Generators/Support/GeneratorTestCompiler.cs b/tests/SquidStd.Tests/Generators/Support/GeneratorTestCompiler.cs index 8fbabee1..f5580150 100644 --- a/tests/SquidStd.Tests/Generators/Support/GeneratorTestCompiler.cs +++ b/tests/SquidStd.Tests/Generators/Support/GeneratorTestCompiler.cs @@ -33,10 +33,11 @@ params IIncrementalGenerator[] generators "SquidStdGeneratorTests", new[] { syntaxTree }, references, - new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary) + new(OutputKind.DynamicallyLinkedLibrary) ); ISourceGenerator[] selectedGenerators; + if (generators.Length == 0) { selectedGenerators = new[] { new EventListenerRegistrationGenerator().AsSourceGenerator() }; @@ -60,10 +61,10 @@ private static IReadOnlyList CreateReferences() { var trustedPlatformAssemblies = (string?)AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES") ?? string.Empty; var references = trustedPlatformAssemblies - .Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries) - .Select(path => MetadataReference.CreateFromFile(path)) - .Cast() - .ToList(); + .Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries) + .Select(path => MetadataReference.CreateFromFile(path)) + .Cast() + .ToList(); AddReference(references, typeof(IEvent).Assembly.Location); AddReference(references, typeof(RegisterEventListenerAttribute).Assembly.Location); diff --git a/tests/SquidStd.Tests/Health/HealthCheckServiceTests.cs b/tests/SquidStd.Tests/Health/HealthCheckServiceTests.cs index cd462d69..304c469c 100644 --- a/tests/SquidStd.Tests/Health/HealthCheckServiceTests.cs +++ b/tests/SquidStd.Tests/Health/HealthCheckServiceTests.cs @@ -57,13 +57,12 @@ public async Task CheckThatThrows_IsCapturedAsUnhealthy_AndDoesNotBreakOthers() [Fact] public void Ctor_NonPositiveTimeout_Throws() - { - Assert.Throws(() => new HealthCheckService( + => Assert.Throws( + () => new HealthCheckService( [], - new HealthCheckOptions { CheckTimeout = TimeSpan.Zero } + new() { CheckTimeout = TimeSpan.Zero } ) ); - } [Fact] public async Task DuplicateNames_AreMadeUnique() @@ -113,12 +112,8 @@ public async Task OneUnhealthy_MakesOverallUnhealthy() } private static HealthCheckService NewService(HealthCheckOptions options, params IHealthCheck[] checks) - { - return new HealthCheckService(checks, options); - } + => new(checks, options); private static HealthCheckOptions Options(double timeoutSeconds = 5) - { - return new HealthCheckOptions { CheckTimeout = TimeSpan.FromSeconds(timeoutSeconds) }; - } + => new() { CheckTimeout = TimeSpan.FromSeconds(timeoutSeconds) }; } diff --git a/tests/SquidStd.Tests/Integration/Mail/GreenMailContainerFixture.cs b/tests/SquidStd.Tests/Integration/Mail/GreenMailContainerFixture.cs index 60152131..c6d4d846 100644 --- a/tests/SquidStd.Tests/Integration/Mail/GreenMailContainerFixture.cs +++ b/tests/SquidStd.Tests/Integration/Mail/GreenMailContainerFixture.cs @@ -7,16 +7,16 @@ namespace SquidStd.Tests.Integration.Mail; public sealed class GreenMailContainerFixture : IAsyncLifetime { private readonly IContainer _container = new ContainerBuilder() - .WithImage("greenmail/standalone:2.1.0") - .WithEnvironment( - "GREENMAIL_OPTS", - "-Dgreenmail.setup.test.all -Dgreenmail.hostname=0.0.0.0 -Dgreenmail.auth.disabled -Dgreenmail.verbose" - ) - .WithPortBinding(3025, true) - .WithPortBinding(3143, true) - .WithPortBinding(3110, true) - .WithWaitStrategy(Wait.ForUnixContainer().UntilInternalTcpPortIsAvailable(3143)) - .Build(); + .WithImage("greenmail/standalone:2.1.0") + .WithEnvironment( + "GREENMAIL_OPTS", + "-Dgreenmail.setup.test.all -Dgreenmail.hostname=0.0.0.0 -Dgreenmail.auth.disabled -Dgreenmail.verbose" + ) + .WithPortBinding(3025, true) + .WithPortBinding(3143, true) + .WithPortBinding(3110, true) + .WithWaitStrategy(Wait.ForUnixContainer().UntilInternalTcpPortIsAvailable(3143)) + .Build(); public string Host => _container.Hostname; @@ -27,14 +27,10 @@ public sealed class GreenMailContainerFixture : IAsyncLifetime public int Pop3Port => _container.GetMappedPublicPort(3110); public Task DisposeAsync() - { - return _container.DisposeAsync().AsTask(); - } + => _container.DisposeAsync().AsTask(); public Task InitializeAsync() - { - return _container.StartAsync(); - } + => _container.StartAsync(); } [CollectionDefinition(Name)] diff --git a/tests/SquidStd.Tests/Integration/Mail/MailPollingServiceTests.cs b/tests/SquidStd.Tests/Integration/Mail/MailPollingServiceTests.cs index 52646060..c3c1d00b 100644 --- a/tests/SquidStd.Tests/Integration/Mail/MailPollingServiceTests.cs +++ b/tests/SquidStd.Tests/Integration/Mail/MailPollingServiceTests.cs @@ -2,7 +2,6 @@ using MailKit.Security; using MimeKit; using SquidStd.Core.Interfaces.Events; -using SquidStd.Mail.Abstractions.Data.Config; using SquidStd.Mail.Abstractions.Data.Events; using SquidStd.Mail.Abstractions.Types.Mail; using SquidStd.Mail.MailKit.Services; @@ -45,7 +44,7 @@ public async Task PollOnce_PublishesMailReceivedEvent() eventBus.RegisterListener(new DelegateListener(e => received.TrySetResult(e))); var reader = new ImapMailReader( - new MailOptions + new() { Protocol = MailProtocolType.Imap, Host = _fixture.Host, @@ -60,7 +59,7 @@ public async Task PollOnce_PublishesMailReceivedEvent() reader, eventBus, new FakeTimerService(), - new MailOptions { PollIntervalSeconds = 60 } + new() { PollIntervalSeconds = 60 } ); await service.PollOnceAsync(); diff --git a/tests/SquidStd.Tests/Integration/Mail/MailQueueIntegrationTests.cs b/tests/SquidStd.Tests/Integration/Mail/MailQueueIntegrationTests.cs index be7b422f..83fa0fc4 100644 --- a/tests/SquidStd.Tests/Integration/Mail/MailQueueIntegrationTests.cs +++ b/tests/SquidStd.Tests/Integration/Mail/MailQueueIntegrationTests.cs @@ -1,7 +1,6 @@ using DryIoc; using SquidStd.Core.Interfaces.Events; using SquidStd.Mail.Abstractions.Data; -using SquidStd.Mail.Abstractions.Data.Config; using SquidStd.Mail.Abstractions.Types.Mail; using SquidStd.Mail.MailKit.Extensions; using SquidStd.Mail.MailKit.Services; @@ -33,25 +32,25 @@ public async Task Enqueue_IsSentByConsumer_AndDelivered() var container = new Container(); container.RegisterInstance(new EventBusService()); container.AddInMemoryMessaging(); - container.AddMailSender(new SmtpOptions { Host = _fixture.Host, Port = _fixture.SmtpPort, UseSsl = false }); + container.AddMailSender(new() { Host = _fixture.Host, Port = _fixture.SmtpPort, UseSsl = false }); container.AddMailQueue(); var consumer = container.Resolve(); await consumer.StartAsync(); await container.Resolve() - .EnqueueAsync( - new OutgoingMailMessage - { - From = new MailAddress("Sender", "sender@example.com"), - To = [new MailAddress("Target", recipient)], - Subject = "queued-subject", - TextBody = "hi" - } - ); + .EnqueueAsync( + new() + { + From = new("Sender", "sender@example.com"), + To = [new("Target", recipient)], + Subject = "queued-subject", + TextBody = "hi" + } + ); var reader = new ImapMailReader( - new MailOptions + new() { Protocol = MailProtocolType.Imap, Host = _fixture.Host, diff --git a/tests/SquidStd.Tests/Integration/Mail/MailReaderTests.cs b/tests/SquidStd.Tests/Integration/Mail/MailReaderTests.cs index 392156f3..e465f03b 100644 --- a/tests/SquidStd.Tests/Integration/Mail/MailReaderTests.cs +++ b/tests/SquidStd.Tests/Integration/Mail/MailReaderTests.cs @@ -43,7 +43,7 @@ public async Task Pop3_FetchesNewMessage_WithDelete() var user = "pop-user@example.com"; await SendAsync(user, "pop-subject", false); var reader = new Pop3MailReader( - new MailOptions + new() { Protocol = MailProtocolType.Pop3, Host = _fixture.Host, @@ -63,8 +63,7 @@ public async Task Pop3_FetchesNewMessage_WithDelete() } private MailOptions ImapOptions(string user) - { - return new MailOptions + => new() { Protocol = MailProtocolType.Imap, Host = _fixture.Host, @@ -75,7 +74,6 @@ private MailOptions ImapOptions(string user) MarkAsSeen = true, IncludeRawEml = true }; - } private async Task SendAsync(string to, string subject, bool withAttachment) { diff --git a/tests/SquidStd.Tests/Integration/Mail/MailSenderTests.cs b/tests/SquidStd.Tests/Integration/Mail/MailSenderTests.cs index ad1d5e7c..8c92e31c 100644 --- a/tests/SquidStd.Tests/Integration/Mail/MailSenderTests.cs +++ b/tests/SquidStd.Tests/Integration/Mail/MailSenderTests.cs @@ -1,7 +1,5 @@ using System.Text; using SquidStd.Core.Interfaces.Events; -using SquidStd.Mail.Abstractions.Data; -using SquidStd.Mail.Abstractions.Data.Config; using SquidStd.Mail.Abstractions.Data.Events; using SquidStd.Mail.Abstractions.Exceptions; using SquidStd.Mail.Abstractions.Types.Mail; @@ -31,27 +29,27 @@ public async Task SendAsync_DeliversMessage_AndPublishesMailSent() eventBus.RegisterListener(new DelegateListener(e => sent.TrySetResult(e))); var sender = new MailKitMailSender( - new SmtpOptions { Host = _fixture.Host, Port = _fixture.SmtpPort, UseSsl = false }, + new() { Host = _fixture.Host, Port = _fixture.SmtpPort, UseSsl = false }, eventBus ); await sender.SendAsync( - new OutgoingMailMessage - { - From = new MailAddress("Sender", "sender@example.com"), - To = [new MailAddress("Target", recipient)], - Subject = "sent-subject", - TextBody = "hello", - Attachments = [new OutgoingAttachment("a.txt", "text/plain", Encoding.UTF8.GetBytes("xyz"))] - } - ) - .WaitAsync(Timeout); + new() + { + From = new("Sender", "sender@example.com"), + To = [new("Target", recipient)], + Subject = "sent-subject", + TextBody = "hello", + Attachments = [new("a.txt", "text/plain", Encoding.UTF8.GetBytes("xyz"))] + } + ) + .WaitAsync(Timeout); var sentEvent = await sent.Task.WaitAsync(Timeout); Assert.Equal("sent-subject", sentEvent.Subject); var reader = new ImapMailReader( - new MailOptions + new() { Protocol = MailProtocolType.Imap, Host = _fixture.Host, @@ -73,17 +71,18 @@ public async Task SendAsync_Throws_AndPublishesFailed_OnClosedPort() var failed = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); eventBus.RegisterListener(new DelegateListener(e => failed.TrySetResult(e))); - var sender = new MailKitMailSender(new SmtpOptions { Host = _fixture.Host, Port = 1, UseSsl = false }, eventBus); - - await Assert.ThrowsAsync(() => sender.SendAsync( - new OutgoingMailMessage - { - From = new MailAddress("Sender", "sender@example.com"), - To = [new MailAddress("Target", "x@example.com")], - Subject = "fail-subject" - } - ) - .WaitAsync(Timeout) + var sender = new MailKitMailSender(new() { Host = _fixture.Host, Port = 1, UseSsl = false }, eventBus); + + await Assert.ThrowsAsync( + () => sender.SendAsync( + new() + { + From = new("Sender", "sender@example.com"), + To = [new("Target", "x@example.com")], + Subject = "fail-subject" + } + ) + .WaitAsync(Timeout) ); var failedEvent = await failed.Task.WaitAsync(Timeout); diff --git a/tests/SquidStd.Tests/Integration/Search/ElasticSearchServiceTests.cs b/tests/SquidStd.Tests/Integration/Search/ElasticSearchServiceTests.cs index 3739755f..1cf2c7d7 100644 --- a/tests/SquidStd.Tests/Integration/Search/ElasticSearchServiceTests.cs +++ b/tests/SquidStd.Tests/Integration/Search/ElasticSearchServiceTests.cs @@ -1,7 +1,6 @@ using DryIoc; using SquidStd.Search.Abstractions.Attributes; using SquidStd.Search.Abstractions.Interfaces; -using SquidStd.Search.Elasticsearch.Data.Config; using SquidStd.Search.Elasticsearch.Extensions; using SquidStd.Search.Elasticsearch.Linq; @@ -86,16 +85,16 @@ public async Task Query_Range_Order_Take() { var search = NewService(); await search.IndexManyAsync( - [new Order("a", "open", 50, "A"), new Order("b", "open", 200, "B"), new Order("c", "open", 300, "C")], - true - ) - .WaitAsync(Timeout); + [new("a", "open", 50, "A"), new("b", "open", 200, "B"), new Order("c", "open", 300, "C")], + true + ) + .WaitAsync(Timeout); var results = await search.Query() - .Where(o => o.Total > 100) - .OrderByDescending(o => o.Total) - .Take(1) - .ToListAsync(); + .Where(o => o.Total > 100) + .OrderByDescending(o => o.Total) + .Take(1) + .ToListAsync(); Assert.Single(results); Assert.Equal("c", results[0].Id); @@ -104,7 +103,7 @@ await search.IndexManyAsync( private ISearchService NewService() { var container = new Container(); - container.AddElasticsearch(new ElasticsearchOptions { Uri = new Uri(_fixture.ConnectionString) }); + container.AddElasticsearch(new() { Uri = new(_fixture.ConnectionString) }); return container.Resolve(); } diff --git a/tests/SquidStd.Tests/Integration/Search/ElasticsearchContainerFixture.cs b/tests/SquidStd.Tests/Integration/Search/ElasticsearchContainerFixture.cs index 4a4af548..0db6a967 100644 --- a/tests/SquidStd.Tests/Integration/Search/ElasticsearchContainerFixture.cs +++ b/tests/SquidStd.Tests/Integration/Search/ElasticsearchContainerFixture.cs @@ -5,37 +5,34 @@ namespace SquidStd.Tests.Integration.Search; /// -/// Starts a single-node Elasticsearch container once for the collection (security disabled → plain HTTP) and -/// exposes its URI. The default Elasticsearch wait strategy probes over https; we override it to poll http so -/// startup completes against the security-disabled node. +/// Starts a single-node Elasticsearch container once for the collection (security disabled → plain HTTP) and +/// exposes its URI. The default Elasticsearch wait strategy probes over https; we override it to poll http so +/// startup completes against the security-disabled node. /// public sealed class ElasticsearchContainerFixture : IAsyncLifetime { private readonly ElasticsearchContainer _container = new ElasticsearchBuilder() - .WithEnvironment("xpack.security.enabled", "false") - .WithWaitStrategy( - Wait.ForUnixContainer() - .UntilHttpRequestIsSucceeded(request => - request.ForPort(9200) - .ForPath("/_cluster/health") - .ForStatusCode(HttpStatusCode.OK) - ) - ) - .Build(); + .WithEnvironment("xpack.security.enabled", "false") + .WithWaitStrategy( + Wait.ForUnixContainer() + .UntilHttpRequestIsSucceeded( + request => + request.ForPort(9200) + .ForPath("/_cluster/health") + .ForStatusCode(HttpStatusCode.OK) + ) + ) + .Build(); // Security is disabled, so the node serves plain HTTP with no auth. GetConnectionString() still returns // an https:// URL with credentials for 8.x images, so build the endpoint explicitly instead. public string ConnectionString => $"http://{_container.Hostname}:{_container.GetMappedPublicPort(9200)}"; public Task DisposeAsync() - { - return _container.DisposeAsync().AsTask(); - } + => _container.DisposeAsync().AsTask(); public Task InitializeAsync() - { - return _container.StartAsync(); - } + => _container.StartAsync(); } [CollectionDefinition(Name)] diff --git a/tests/SquidStd.Tests/Integration/Templates/TemplatePackTests.cs b/tests/SquidStd.Tests/Integration/Templates/TemplatePackTests.cs index f0c7e7a0..e2a063b7 100644 --- a/tests/SquidStd.Tests/Integration/Templates/TemplatePackTests.cs +++ b/tests/SquidStd.Tests/Integration/Templates/TemplatePackTests.cs @@ -3,9 +3,9 @@ namespace SquidStd.Tests.Integration.Templates; /// -/// Packs SquidStd.Templates, installs it into an isolated dotnet-new hive, instantiates each template, and -/// asserts the generated output (name substitution, version-sentinel replacement, messaging branch). No build -/// of the generated projects: the referenced SquidStd.* packages may not be published yet. +/// Packs SquidStd.Templates, installs it into an isolated dotnet-new hive, instantiates each template, and +/// asserts the generated output (name substitution, version-sentinel replacement, messaging branch). No build +/// of the generated projects: the referenced SquidStd.* packages may not be published yet. /// public sealed class TemplatePackTests : IDisposable { @@ -37,12 +37,12 @@ public TemplatePackTests() Assert.True(TryRun("dotnet", $"pack \"{project}\" -c Release", _repoRoot, out _), "pack failed"); var nupkg = Directory - .GetFiles( - Path.Combine(_repoRoot, "src", "SquidStd.Templates", "bin", "Release"), - "SquidStd.Templates.*.nupkg" - ) - .OrderByDescending(File.GetLastWriteTimeUtc) - .First(); + .GetFiles( + Path.Combine(_repoRoot, "src", "SquidStd.Templates", "bin", "Release"), + "SquidStd.Templates.*.nupkg" + ) + .OrderByDescending(File.GetLastWriteTimeUtc) + .First(); _installed = TryRun("dotnet", $"new install \"{nupkg}\" --debug:custom-hive \"{_hive}\"", _repoRoot, out _); } diff --git a/tests/SquidStd.Tests/Integration/Workers/WorkerSystemIntegrationTests.cs b/tests/SquidStd.Tests/Integration/Workers/WorkerSystemIntegrationTests.cs index 05881c5d..cc5002f7 100644 --- a/tests/SquidStd.Tests/Integration/Workers/WorkerSystemIntegrationTests.cs +++ b/tests/SquidStd.Tests/Integration/Workers/WorkerSystemIntegrationTests.cs @@ -15,9 +15,9 @@ namespace SquidStd.Tests.Integration.Workers; /// -/// End-to-end test of the workers system over a real RabbitMQ broker (Testcontainers): the manager -/// enqueues a job that the worker consumes and runs (real queue), and the worker's heartbeat reaches the -/// manager's registry (real fan-out topic). +/// End-to-end test of the workers system over a real RabbitMQ broker (Testcontainers): the manager +/// enqueues a job that the worker consumes and runs (real queue), and the worker's heartbeat reaches the +/// manager's registry (real fan-out topic). /// [Collection(RabbitMqCollection.Name)] public class WorkerSystemIntegrationTests @@ -36,7 +36,7 @@ public async Task Manager_EnqueuesJob_WorkerRunsIt_AndHeartbeatReachesRegistry() { using var container = new Container(); container.RegisterInstance(new EventBusService()); - container.AddRabbitMqMessaging(new RabbitMqOptions { Uri = new Uri(_fixture.AmqpUri) }); + container.AddRabbitMqMessaging(new RabbitMqOptions { Uri = new(_fixture.AmqpUri) }); // Unique channel names per run so parallel/other tests on the shared broker do not interfere. var suffix = Guid.NewGuid().ToString("N"); diff --git a/tests/SquidStd.Tests/Json/JsonContextTypeResolverTests.cs b/tests/SquidStd.Tests/Json/JsonContextTypeResolverTests.cs index ef40a5b4..bb476cef 100644 --- a/tests/SquidStd.Tests/Json/JsonContextTypeResolverTests.cs +++ b/tests/SquidStd.Tests/Json/JsonContextTypeResolverTests.cs @@ -43,15 +43,11 @@ public void GetTypeInfo_RegisteredType_ReturnsTypeInfo() [Fact] public void IsTypeRegistered_RegisteredType_ReturnsTrue() - { - Assert.True(JsonContextTypeResolver.IsTypeRegistered(TestJsonContext.Default, typeof(SampleDto))); - } + => Assert.True(JsonContextTypeResolver.IsTypeRegistered(TestJsonContext.Default, typeof(SampleDto))); [Fact] public void IsTypeRegistered_UnregisteredType_ReturnsFalse() - { - Assert.False( + => Assert.False( JsonContextTypeResolver.IsTypeRegistered(TestJsonContext.Default, typeof(JsonContextTypeResolverTests)) ); - } } diff --git a/tests/SquidStd.Tests/Json/JsonUtilsTests.cs b/tests/SquidStd.Tests/Json/JsonUtilsTests.cs index 4420d268..b26f3add 100644 --- a/tests/SquidStd.Tests/Json/JsonUtilsTests.cs +++ b/tests/SquidStd.Tests/Json/JsonUtilsTests.cs @@ -35,17 +35,11 @@ public void AddAndRemoveJsonConverter_MutatesConverterList() [Fact] public void Deserialize_InvalidJson_ThrowsJsonException() - { - Assert.Throws(() => JsonUtils.Deserialize("{ not valid")); - } + => Assert.Throws(() => JsonUtils.Deserialize("{ not valid")); - [Theory] - [InlineData("")] - [InlineData(" ")] + [Theory, InlineData(""), InlineData(" ")] public void Deserialize_NullOrWhitespace_Throws(string json) - { - Assert.Throws(() => JsonUtils.Deserialize(json)); - } + => Assert.Throws(() => JsonUtils.Deserialize(json)); [Fact] public void Deserialize_WithExplicitContext_ReturnsObject() @@ -60,11 +54,10 @@ public void Deserialize_WithExplicitContext_ReturnsObject() [Fact] public void DeserializeFromFile_MissingFile_Throws() - { - Assert.Throws(() => - JsonUtils.DeserializeFromFile(Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".json")) + => Assert.Throws( + () => + JsonUtils.DeserializeFromFile(Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".json")) ); - } [Fact] public void DeserializeOrDefault_EmptyJson_ReturnsDefault() @@ -87,13 +80,9 @@ public void DeserializeOrDefault_ValidJson_ReturnsObject() [Fact] public void GetJsonConverters_ContainsDefaultEnumConverter() - { - Assert.Contains(JsonUtils.GetJsonConverters(), converter => converter is JsonStringEnumConverter); - } + => Assert.Contains(JsonUtils.GetJsonConverters(), converter => converter is JsonStringEnumConverter); - [Theory] - [InlineData("UserEntity", "user.schema.json")] - [InlineData("SampleDto", "sample_dto.schema.json")] + [Theory, InlineData("UserEntity", "user.schema.json"), InlineData("SampleDto", "sample_dto.schema.json")] public void GetSchemaFileName_GeneratesSnakeCaseSchemaName(string typeName, string expected) { // Map the parameterized type name to a real type with the same Name. @@ -102,30 +91,18 @@ public void GetSchemaFileName_GeneratesSnakeCaseSchemaName(string typeName, stri Assert.Equal(expected, JsonUtils.GetSchemaFileName(type)); } - [Theory] - [InlineData("[1,2,3]", true)] - [InlineData("{\"a\":1}", false)] - [InlineData("invalid", false)] + [Theory, InlineData("[1,2,3]", true), InlineData("{\"a\":1}", false), InlineData("invalid", false)] public void IsArray_VariousInputs_ReturnsExpected(string json, bool expected) - { - Assert.Equal(expected, JsonUtils.IsArray(json)); - } + => Assert.Equal(expected, JsonUtils.IsArray(json)); - [Theory] - [InlineData("{\"a\":1}", true)] - [InlineData("[1,2,3]", true)] - [InlineData("not json", false)] - [InlineData("", false)] + [Theory, InlineData("{\"a\":1}", true), InlineData("[1,2,3]", true), InlineData("not json", false), + InlineData("", false)] public void IsValidJson_VariousInputs_ReturnsExpected(string json, bool expected) - { - Assert.Equal(expected, JsonUtils.IsValidJson(json)); - } + => Assert.Equal(expected, JsonUtils.IsValidJson(json)); [Fact] public void Serialize_NullObject_Throws() - { - Assert.Throws(() => JsonUtils.Serialize(null!)); - } + => Assert.Throws(() => JsonUtils.Serialize(null!)); [Fact] public void SerializeDeserialize_RoundTrip_PreservesValues() @@ -155,7 +132,5 @@ public void SerializeToFile_DeserializeFromFile_RoundTrips() } // Local type whose name ends with "Entity" to verify suffix stripping. - private sealed class UserEntity - { - } + private sealed class UserEntity { } } diff --git a/tests/SquidStd.Tests/Logging/EventSinkExtensionsTests.cs b/tests/SquidStd.Tests/Logging/EventSinkExtensionsTests.cs index c3dd804f..8e2aa0a5 100644 --- a/tests/SquidStd.Tests/Logging/EventSinkExtensionsTests.cs +++ b/tests/SquidStd.Tests/Logging/EventSinkExtensionsTests.cs @@ -13,16 +13,14 @@ public void EventSink_WiredIntoSerilog_RaisesEventsForLogs() LogEventData? captured = null; void Handler(object? sender, LogEventData data) - { - captured = data; - } + => captured = data; EventSink.OnLogReceived += Handler; var logger = new LoggerConfiguration() - .WriteTo - .EventSink() - .CreateLogger(); + .WriteTo + .EventSink() + .CreateLogger(); try { diff --git a/tests/SquidStd.Tests/Logging/EventSinkTests.cs b/tests/SquidStd.Tests/Logging/EventSinkTests.cs index 6955c382..1a0b90a8 100644 --- a/tests/SquidStd.Tests/Logging/EventSinkTests.cs +++ b/tests/SquidStd.Tests/Logging/EventSinkTests.cs @@ -14,9 +14,7 @@ public void Emit_AfterClearSubscribers_DoesNotInvokeHandler() var invoked = false; void Handler(object? sender, LogEventData data) - { - invoked = true; - } + => invoked = true; EventSink.OnLogReceived += Handler; EventSink.ClearSubscribers(); @@ -50,9 +48,7 @@ public void Emit_WithSubscriber_RaisesEventWithMappedData() LogEventData? captured = null; void Handler(object? sender, LogEventData data) - { - captured = data; - } + => captured = data; EventSink.OnLogReceived += Handler; @@ -84,6 +80,6 @@ private static LogEvent CreateLogEvent(LogEventLevel level, string template, par { var parsedTemplate = new MessageTemplateParser().Parse(template); - return new LogEvent(DateTimeOffset.UtcNow, level, null, parsedTemplate, properties); + return new(DateTimeOffset.UtcNow, level, null, parsedTemplate, properties); } } diff --git a/tests/SquidStd.Tests/Mail/MailQueueTests.cs b/tests/SquidStd.Tests/Mail/MailQueueTests.cs index a1fa7dfd..e962f63e 100644 --- a/tests/SquidStd.Tests/Mail/MailQueueTests.cs +++ b/tests/SquidStd.Tests/Mail/MailQueueTests.cs @@ -25,9 +25,9 @@ public async Task EnqueueAsync_PublishesMessageToConfiguredQueue() var queue = new MailQueue(messageQueue, options); await queue.EnqueueAsync( - new OutgoingMailMessage + new() { - To = [new MailAddress("Bob", "bob@example.com")], + To = [new("Bob", "bob@example.com")], Subject = "queued" } ); diff --git a/tests/SquidStd.Tests/Mail/MailRegistrationExtensionsTests.cs b/tests/SquidStd.Tests/Mail/MailRegistrationExtensionsTests.cs index 725d8860..4c477239 100644 --- a/tests/SquidStd.Tests/Mail/MailRegistrationExtensionsTests.cs +++ b/tests/SquidStd.Tests/Mail/MailRegistrationExtensionsTests.cs @@ -41,8 +41,8 @@ public void AddMail_Throws_OnInvalidHostOrPort() { using var container = NewContainer(); - Assert.Throws(() => container.AddMail(new MailOptions { Host = string.Empty, Port = 993 })); - Assert.Throws(() => container.AddMail(new MailOptions { Host = "h", Port = 0 })); + Assert.Throws(() => container.AddMail(new() { Host = string.Empty, Port = 993 })); + Assert.Throws(() => container.AddMail(new() { Host = "h", Port = 0 })); } private static Container NewContainer() @@ -55,8 +55,5 @@ private static Container NewContainer() } private static MailOptions ValidOptions(MailProtocolType protocol) - { - return new MailOptions - { Protocol = protocol, Host = "mail.example.com", Port = 993, Username = "u", Password = "p" }; - } + => new() { Protocol = protocol, Host = "mail.example.com", Port = 993, Username = "u", Password = "p" }; } diff --git a/tests/SquidStd.Tests/Mail/MailSendConsumerServiceTests.cs b/tests/SquidStd.Tests/Mail/MailSendConsumerServiceTests.cs index 1cf74691..3d04adcf 100644 --- a/tests/SquidStd.Tests/Mail/MailSendConsumerServiceTests.cs +++ b/tests/SquidStd.Tests/Mail/MailSendConsumerServiceTests.cs @@ -66,9 +66,7 @@ public async Task Consumer_SendsEnqueuedMessage() } private static OutgoingMailMessage NewMessage() - { - return new OutgoingMailMessage { To = [new MailAddress("Bob", "bob@example.com")], Subject = "queued" }; - } + => new() { To = [new("Bob", "bob@example.com")], Subject = "queued" }; private static async Task WaitUntilAsync(Func condition, TimeSpan timeout) { diff --git a/tests/SquidStd.Tests/Mail/MailSenderRegistrationExtensionsTests.cs b/tests/SquidStd.Tests/Mail/MailSenderRegistrationExtensionsTests.cs index 5b7d57ab..94308654 100644 --- a/tests/SquidStd.Tests/Mail/MailSenderRegistrationExtensionsTests.cs +++ b/tests/SquidStd.Tests/Mail/MailSenderRegistrationExtensionsTests.cs @@ -1,6 +1,5 @@ using DryIoc; using SquidStd.Core.Interfaces.Events; -using SquidStd.Mail.Abstractions.Data.Config; using SquidStd.Mail.Abstractions.Interfaces; using SquidStd.Mail.MailKit.Extensions; using SquidStd.Mail.MailKit.Services; @@ -15,7 +14,7 @@ public void AddMailSender_RegistersSender() { using var container = NewContainer(); - container.AddMailSender(new SmtpOptions { Host = "smtp.example.com", Port = 587 }); + container.AddMailSender(new() { Host = "smtp.example.com", Port = 587 }); Assert.IsType(container.Resolve()); } @@ -25,8 +24,8 @@ public void AddMailSender_Throws_OnInvalidHostOrPort() { using var container = NewContainer(); - Assert.Throws(() => container.AddMailSender(new SmtpOptions { Host = string.Empty, Port = 587 })); - Assert.Throws(() => container.AddMailSender(new SmtpOptions { Host = "h", Port = 0 })); + Assert.Throws(() => container.AddMailSender(new() { Host = string.Empty, Port = 587 })); + Assert.Throws(() => container.AddMailSender(new() { Host = "h", Port = 0 })); } private static Container NewContainer() diff --git a/tests/SquidStd.Tests/Mail/MimeMessageMapperTests.cs b/tests/SquidStd.Tests/Mail/MimeMessageMapperTests.cs index c72d7c0d..3c59034d 100644 --- a/tests/SquidStd.Tests/Mail/MimeMessageMapperTests.cs +++ b/tests/SquidStd.Tests/Mail/MimeMessageMapperTests.cs @@ -55,7 +55,7 @@ private static MimeMessage BuildMessage() message.To.Add(new MailboxAddress("Bob", "bob@example.com")); message.Cc.Add(new MailboxAddress("Carol", "carol@example.com")); message.Subject = "Hello"; - message.Date = new DateTimeOffset(2026, 6, 24, 10, 0, 0, TimeSpan.Zero); + message.Date = new(2026, 6, 24, 10, 0, 0, TimeSpan.Zero); message.MessageId = "msg-1@example.com"; var builder = new BodyBuilder diff --git a/tests/SquidStd.Tests/Mail/OutgoingMessageMapperTests.cs b/tests/SquidStd.Tests/Mail/OutgoingMessageMapperTests.cs index a8ff5f22..29bb27c0 100644 --- a/tests/SquidStd.Tests/Mail/OutgoingMessageMapperTests.cs +++ b/tests/SquidStd.Tests/Mail/OutgoingMessageMapperTests.cs @@ -11,7 +11,7 @@ public class OutgoingMessageMapperTests [Fact] public void ToMimeMessage_FallsBackToDefaultFrom() { - var options = new SmtpOptions { DefaultFrom = new MailAddress("Sys", "sys@example.com") }; + var options = new SmtpOptions { DefaultFrom = new("Sys", "sys@example.com") }; var mime = OutgoingMessageMapper.ToMimeMessage(Message(), options); @@ -21,9 +21,9 @@ public void ToMimeMessage_FallsBackToDefaultFrom() [Fact] public void ToMimeMessage_MapsRecipientsSubjectBodiesAndAttachment() { - var options = new SmtpOptions { DefaultFrom = new MailAddress("Sys", "sys@example.com") }; + var options = new SmtpOptions { DefaultFrom = new("Sys", "sys@example.com") }; - var mime = OutgoingMessageMapper.ToMimeMessage(Message(new MailAddress("Alice", "alice@example.com")), options); + var mime = OutgoingMessageMapper.ToMimeMessage(Message(new("Alice", "alice@example.com")), options); Assert.Equal("alice@example.com", mime.From.Mailboxes.Single().Address); Assert.Contains(mime.To.Mailboxes, m => m.Address == "bob@example.com"); @@ -37,22 +37,18 @@ public void ToMimeMessage_MapsRecipientsSubjectBodiesAndAttachment() [Fact] public void ToMimeMessage_Throws_WhenNoFromAndNoDefault() - { - Assert.Throws(() => OutgoingMessageMapper.ToMimeMessage(Message(), new SmtpOptions())); - } + => Assert.Throws(() => OutgoingMessageMapper.ToMimeMessage(Message(), new())); private static OutgoingMailMessage Message(MailAddress? from = null) - { - return new OutgoingMailMessage + => new() { From = from, - To = [new MailAddress("Bob", "bob@example.com")], - Cc = [new MailAddress("Carol", "carol@example.com")], - Bcc = [new MailAddress("Dave", "dave@example.com")], + To = [new("Bob", "bob@example.com")], + Cc = [new("Carol", "carol@example.com")], + Bcc = [new("Dave", "dave@example.com")], Subject = "Hello", TextBody = "plain", HtmlBody = "

html

", - Attachments = [new OutgoingAttachment("a.txt", "text/plain", Encoding.UTF8.GetBytes("xyz"))] + Attachments = [new("a.txt", "text/plain", Encoding.UTF8.GetBytes("xyz"))] }; - } } diff --git a/tests/SquidStd.Tests/Mail/Support/FakeTimerService.cs b/tests/SquidStd.Tests/Mail/Support/FakeTimerService.cs index deb15def..b27bef21 100644 --- a/tests/SquidStd.Tests/Mail/Support/FakeTimerService.cs +++ b/tests/SquidStd.Tests/Mail/Support/FakeTimerService.cs @@ -14,22 +14,14 @@ public string RegisterTimer(string name, TimeSpan interval, Action callback, Tim return "timer-1"; } - public void UnregisterAllTimers() - { - } + public void UnregisterAllTimers() { } public bool UnregisterTimer(string timerId) - { - return true; - } + => true; public int UnregisterTimersByName(string name) - { - return 0; - } + => 0; public int UpdateTicksDelta(long timestampMilliseconds) - { - return 0; - } + => 0; } diff --git a/tests/SquidStd.Tests/Mail/Support/ThrowingMailSender.cs b/tests/SquidStd.Tests/Mail/Support/ThrowingMailSender.cs index 48ee9089..1f94054c 100644 --- a/tests/SquidStd.Tests/Mail/Support/ThrowingMailSender.cs +++ b/tests/SquidStd.Tests/Mail/Support/ThrowingMailSender.cs @@ -7,7 +7,5 @@ namespace SquidStd.Tests.Mail.Support; public sealed class ThrowingMailSender : IMailSender { public Task SendAsync(OutgoingMailMessage message, CancellationToken cancellationToken = default) - { - throw new InvalidOperationException("send failed"); - } + => throw new InvalidOperationException("send failed"); } diff --git a/tests/SquidStd.Tests/Manager/HeartbeatCollectorServiceTests.cs b/tests/SquidStd.Tests/Manager/HeartbeatCollectorServiceTests.cs index decf3670..62c4d761 100644 --- a/tests/SquidStd.Tests/Manager/HeartbeatCollectorServiceTests.cs +++ b/tests/SquidStd.Tests/Manager/HeartbeatCollectorServiceTests.cs @@ -6,7 +6,6 @@ using SquidStd.Workers.Abstractions; using SquidStd.Workers.Abstractions.Data; using SquidStd.Workers.Abstractions.Types; -using SquidStd.Workers.Manager.Data.Config; using SquidStd.Workers.Manager.Data.Events; using SquidStd.Workers.Manager.Services; @@ -23,12 +22,12 @@ public async Task Collector_RecordsHeartbeat_AndPublishesDiscoveredEvent() container.AddInMemoryMessaging(); var topic = container.Resolve(); - var registry = new WorkerRegistry(new WorkerManagerConfig()); + var registry = new WorkerRegistry(new()); var discovered = new TaskCompletionSource(); eventBus.RegisterListener(new DelegateListener(e => discovered.TrySetResult(e))); - var service = new HeartbeatCollectorService(topic, registry, eventBus, new WorkerManagerConfig()); + var service = new HeartbeatCollectorService(topic, registry, eventBus, new()); await service.StartAsync(); await topic.PublishAsync( diff --git a/tests/SquidStd.Tests/Manager/JobSchedulerTests.cs b/tests/SquidStd.Tests/Manager/JobSchedulerTests.cs index 57c03f32..2f38d528 100644 --- a/tests/SquidStd.Tests/Manager/JobSchedulerTests.cs +++ b/tests/SquidStd.Tests/Manager/JobSchedulerTests.cs @@ -5,7 +5,6 @@ using SquidStd.Services.Core.Services; using SquidStd.Workers.Abstractions; using SquidStd.Workers.Abstractions.Data; -using SquidStd.Workers.Manager.Data.Config; using SquidStd.Workers.Manager.Services; namespace SquidStd.Tests.Manager; @@ -26,7 +25,7 @@ public async Task EnqueueAsync_PublishesJobRequestToConfiguredQueue() new DelegateListener(job => received.TrySetResult(job)) ); - var scheduler = new JobScheduler(queue, new WorkerManagerConfig()); + var scheduler = new JobScheduler(queue, new()); await scheduler.EnqueueAsync("resize", new Dictionary { ["w"] = "100" }); var job = await received.Task.WaitAsync(TimeSpan.FromSeconds(5)); diff --git a/tests/SquidStd.Tests/Manager/Support/FakeTimerService.cs b/tests/SquidStd.Tests/Manager/Support/FakeTimerService.cs index 3867541a..edd54854 100644 --- a/tests/SquidStd.Tests/Manager/Support/FakeTimerService.cs +++ b/tests/SquidStd.Tests/Manager/Support/FakeTimerService.cs @@ -22,9 +22,7 @@ public string RegisterTimer(string name, TimeSpan interval, Action callback, Tim return "timer-1"; } - public void UnregisterAllTimers() - { - } + public void UnregisterAllTimers() { } public bool UnregisterTimer(string timerId) { @@ -34,12 +32,8 @@ public bool UnregisterTimer(string timerId) } public int UnregisterTimersByName(string name) - { - return 0; - } + => 0; public int UpdateTicksDelta(long timestampMilliseconds) - { - return 0; - } + => 0; } diff --git a/tests/SquidStd.Tests/Manager/WorkerManagerEndpointsTests.cs b/tests/SquidStd.Tests/Manager/WorkerManagerEndpointsTests.cs index d4bf2dcd..a6cd82d1 100644 --- a/tests/SquidStd.Tests/Manager/WorkerManagerEndpointsTests.cs +++ b/tests/SquidStd.Tests/Manager/WorkerManagerEndpointsTests.cs @@ -6,8 +6,6 @@ using SquidStd.Services.Core.Services; using SquidStd.Workers.Abstractions.Data; using SquidStd.Workers.Abstractions.Types; -using SquidStd.Workers.Manager.Data; -using SquidStd.Workers.Manager.Data.Config; using SquidStd.Workers.Manager.Endpoints; using SquidStd.Workers.Manager.Services; @@ -19,10 +17,10 @@ public class WorkerManagerEndpointsTests public async Task EnqueueJob_ReturnsAccepted_AndSchedulesJob() { var result = await WorkerManagerEndpoints.EnqueueJob( - new EnqueueJobRequest("resize", new Dictionary()), - NewScheduler(), - CancellationToken.None - ); + new("resize", new Dictionary()), + NewScheduler(), + CancellationToken.None + ); Assert.IsType(result.Result); } @@ -31,10 +29,10 @@ public async Task EnqueueJob_ReturnsAccepted_AndSchedulesJob() public async Task EnqueueJob_ReturnsBadRequest_WhenJobNameBlank() { var result = await WorkerManagerEndpoints.EnqueueJob( - new EnqueueJobRequest(" ", null), - NewScheduler(), - CancellationToken.None - ); + new(" ", null), + NewScheduler(), + CancellationToken.None + ); Assert.IsType>(result.Result); } @@ -69,16 +67,16 @@ private static JobScheduler NewScheduler() container.RegisterInstance(new EventBusService()); container.AddInMemoryMessaging(); - return new JobScheduler(container.Resolve(), new WorkerManagerConfig()); + return new(container.Resolve(), new()); } private static WorkerRegistry RegistryWith(params string[] workerIds) { - var registry = new WorkerRegistry(new WorkerManagerConfig()); + var registry = new WorkerRegistry(new()); foreach (var id in workerIds) { - registry.Record(new WorkerHeartbeat(id, DateTime.UtcNow, WorkerStatusType.Idle, 0, 8)); + registry.Record(new(id, DateTime.UtcNow, WorkerStatusType.Idle, 0, 8)); } return registry; diff --git a/tests/SquidStd.Tests/Manager/WorkerOfflineSweepServiceTests.cs b/tests/SquidStd.Tests/Manager/WorkerOfflineSweepServiceTests.cs index fd0933b1..9b689499 100644 --- a/tests/SquidStd.Tests/Manager/WorkerOfflineSweepServiceTests.cs +++ b/tests/SquidStd.Tests/Manager/WorkerOfflineSweepServiceTests.cs @@ -1,9 +1,7 @@ using SquidStd.Core.Interfaces.Events; using SquidStd.Services.Core.Services; using SquidStd.Tests.Manager.Support; -using SquidStd.Workers.Abstractions.Data; using SquidStd.Workers.Abstractions.Types; -using SquidStd.Workers.Manager.Data.Config; using SquidStd.Workers.Manager.Data.Events; using SquidStd.Workers.Manager.Services; @@ -14,15 +12,15 @@ public class WorkerOfflineSweepServiceTests [Fact] public async Task RunSweepAsync_MarksOverdueWorkerOffline_AndPublishesTransition() { - var registry = new WorkerRegistry(new WorkerManagerConfig { OfflineTimeoutSeconds = 1 }); - registry.Record(new WorkerHeartbeat("w1", DateTime.UtcNow, WorkerStatusType.Idle, 0, 8)); + var registry = new WorkerRegistry(new() { OfflineTimeoutSeconds = 1 }); + registry.Record(new("w1", DateTime.UtcNow, WorkerStatusType.Idle, 0, 8)); await Task.Delay(1100); var eventBus = new EventBusService(); var offline = new TaskCompletionSource(); eventBus.RegisterListener(new DelegateListener(e => offline.TrySetResult(e))); - var service = new WorkerOfflineSweepService(new FakeTimerService(), registry, eventBus, new WorkerManagerConfig()); + var service = new WorkerOfflineSweepService(new FakeTimerService(), registry, eventBus, new()); await service.RunSweepAsync(); var change = await offline.Task.WaitAsync(TimeSpan.FromSeconds(5)); @@ -35,12 +33,12 @@ public async Task RunSweepAsync_MarksOverdueWorkerOffline_AndPublishesTransition public async Task StartAsync_RegistersRepeatingTimer_StopAsyncUnregisters() { var timer = new FakeTimerService(); - var registry = new WorkerRegistry(new WorkerManagerConfig()); + var registry = new WorkerRegistry(new()); var service = new WorkerOfflineSweepService( timer, registry, new EventBusService(), - new WorkerManagerConfig { SweepIntervalSeconds = 5 } + new() { SweepIntervalSeconds = 5 } ); await service.StartAsync(); diff --git a/tests/SquidStd.Tests/Manager/WorkerRegistryTests.cs b/tests/SquidStd.Tests/Manager/WorkerRegistryTests.cs index f5a219fd..e2b45c92 100644 --- a/tests/SquidStd.Tests/Manager/WorkerRegistryTests.cs +++ b/tests/SquidStd.Tests/Manager/WorkerRegistryTests.cs @@ -1,6 +1,5 @@ using SquidStd.Workers.Abstractions.Data; using SquidStd.Workers.Abstractions.Types; -using SquidStd.Workers.Manager.Data.Config; using SquidStd.Workers.Manager.Services; namespace SquidStd.Tests.Manager; @@ -90,12 +89,8 @@ public void Sweep_MarksOverdueWorkerOffline_AndReturnsTransition() } private static WorkerHeartbeat Heartbeat(string id, WorkerStatusType status, int activeJobs = 0) - { - return new WorkerHeartbeat(id, DateTime.UtcNow, status, activeJobs, 8); - } + => new(id, DateTime.UtcNow, status, activeJobs, 8); private static WorkerRegistry NewRegistry(int offlineTimeoutSeconds = 30) - { - return new WorkerRegistry(new WorkerManagerConfig { OfflineTimeoutSeconds = offlineTimeoutSeconds }); - } + => new(new() { OfflineTimeoutSeconds = offlineTimeoutSeconds }); } diff --git a/tests/SquidStd.Tests/Messaging/InMemoryQueueProviderTests.cs b/tests/SquidStd.Tests/Messaging/InMemoryQueueProviderTests.cs index 4bd45577..d1f9e2f8 100644 --- a/tests/SquidStd.Tests/Messaging/InMemoryQueueProviderTests.cs +++ b/tests/SquidStd.Tests/Messaging/InMemoryQueueProviderTests.cs @@ -12,7 +12,7 @@ public class InMemoryQueueProviderTests [Fact] public async Task AlwaysFailing_IsDeadLetteredAfterMaxAttempts() { - await using var provider = NewProvider(options: new MessagingOptions { MaxDeliveryAttempts = 2 }); + await using var provider = NewProvider(options: new() { MaxDeliveryAttempts = 2 }); var attempts = 0; provider.Subscribe( "q", @@ -66,7 +66,7 @@ public async Task DisposedSubscription_StopsReceiving() [Fact] public async Task FailingThenSucceeding_IsRetriedAndDelivered() { - await using var provider = NewProvider(options: new MessagingOptions { MaxDeliveryAttempts = 3 }); + await using var provider = NewProvider(options: new() { MaxDeliveryAttempts = 3 }); var attempts = 0; var delivered = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); provider.Subscribe( @@ -169,20 +169,14 @@ public async Task TwoSubscribers_ReceiveRoundRobin() } private static ReadOnlyMemory Bytes(string s) - { - return Encoding.UTF8.GetBytes(s); - } + => Encoding.UTF8.GetBytes(s); private static InMemoryQueueProvider NewProvider( MessagingMetricsProvider? metrics = null, MessagingOptions? options = null ) - { - return new InMemoryQueueProvider(options ?? new MessagingOptions(), metrics ?? new MessagingMetricsProvider()); - } + => new(options ?? new MessagingOptions(), metrics ?? new MessagingMetricsProvider()); private static string Text(ReadOnlyMemory b) - { - return Encoding.UTF8.GetString(b.Span); - } + => Encoding.UTF8.GetString(b.Span); } diff --git a/tests/SquidStd.Tests/Messaging/InMemoryTopicProviderTests.cs b/tests/SquidStd.Tests/Messaging/InMemoryTopicProviderTests.cs index 97b02930..888e1224 100644 --- a/tests/SquidStd.Tests/Messaging/InMemoryTopicProviderTests.cs +++ b/tests/SquidStd.Tests/Messaging/InMemoryTopicProviderTests.cs @@ -87,7 +87,5 @@ public async Task Publish_NoSubscribers_IsNoOp() } private static ReadOnlyMemory Bytes(string s) - { - return Encoding.UTF8.GetBytes(s); - } + => Encoding.UTF8.GetBytes(s); } diff --git a/tests/SquidStd.Tests/Messaging/MessageQueueTests.cs b/tests/SquidStd.Tests/Messaging/MessageQueueTests.cs index 78148e57..c0f71e00 100644 --- a/tests/SquidStd.Tests/Messaging/MessageQueueTests.cs +++ b/tests/SquidStd.Tests/Messaging/MessageQueueTests.cs @@ -1,5 +1,4 @@ using SquidStd.Core.Json; -using SquidStd.Messaging.Abstractions.Data.Config; using SquidStd.Messaging.Abstractions.Interfaces; using SquidStd.Messaging.Abstractions.Services; using SquidStd.Messaging.Services; @@ -13,7 +12,7 @@ public class MessageQueueTests [Fact] public async Task PublishAsync_DeliversTypedMessageToListener() { - await using var provider = new InMemoryQueueProvider(new MessagingOptions(), new MessagingMetricsProvider()); + await using var provider = new InMemoryQueueProvider(new(), new MessagingMetricsProvider()); var serializer = new JsonDataSerializer(); IMessageQueue queue = new MessageQueue(provider, serializer, serializer); var listener = new CapturingListener(); diff --git a/tests/SquidStd.Tests/Messaging/MessagingConnectionStringTests.cs b/tests/SquidStd.Tests/Messaging/MessagingConnectionStringTests.cs index f82c8b0a..57be3c85 100644 --- a/tests/SquidStd.Tests/Messaging/MessagingConnectionStringTests.cs +++ b/tests/SquidStd.Tests/Messaging/MessagingConnectionStringTests.cs @@ -16,9 +16,7 @@ public void Parse_Memory_ReadsScheme() [Fact] public void Parse_NullOrWhitespace_Throws() - { - Assert.Throws(() => MessagingConnectionString.Parse(" ")); - } + => Assert.Throws(() => MessagingConnectionString.Parse(" ")); [Fact] public void Parse_RabbitMq_ReadsCredentialsHostPortVhost() diff --git a/tests/SquidStd.Tests/Messaging/MessagingMetricsProviderTests.cs b/tests/SquidStd.Tests/Messaging/MessagingMetricsProviderTests.cs index ea163748..4cf10819 100644 --- a/tests/SquidStd.Tests/Messaging/MessagingMetricsProviderTests.cs +++ b/tests/SquidStd.Tests/Messaging/MessagingMetricsProviderTests.cs @@ -21,9 +21,7 @@ public async Task CollectAsync_ReportsCountersAndGaugesPerQueue() var samples = await metrics.CollectAsync(); double Value(string name) - { - return samples.Single(s => s.Name == name && s.Tags != null && s.Tags["queue"] == "orders").Value; - } + => samples.Single(s => s.Name == name && s.Tags != null && s.Tags["queue"] == "orders").Value; Assert.Equal(2, Value("published")); Assert.Equal(1, Value("delivered")); @@ -36,7 +34,5 @@ double Value(string name) [Fact] public void ProviderName_IsMessaging() - { - Assert.Equal("messaging", new MessagingMetricsProvider().ProviderName); - } + => Assert.Equal("messaging", new MessagingMetricsProvider().ProviderName); } diff --git a/tests/SquidStd.Tests/Messaging/RabbitMq/RabbitMqContainerFixture.cs b/tests/SquidStd.Tests/Messaging/RabbitMq/RabbitMqContainerFixture.cs index f30af310..3a1291f0 100644 --- a/tests/SquidStd.Tests/Messaging/RabbitMq/RabbitMqContainerFixture.cs +++ b/tests/SquidStd.Tests/Messaging/RabbitMq/RabbitMqContainerFixture.cs @@ -3,7 +3,7 @@ namespace SquidStd.Tests.Messaging.RabbitMq; /// -/// Starts a RabbitMQ container once for the whole collection and exposes its AMQP URI. +/// Starts a RabbitMQ container once for the whole collection and exposes its AMQP URI. /// public sealed class RabbitMqContainerFixture : IAsyncLifetime { @@ -12,14 +12,10 @@ public sealed class RabbitMqContainerFixture : IAsyncLifetime public string AmqpUri => _container.GetConnectionString(); public Task DisposeAsync() - { - return _container.DisposeAsync().AsTask(); - } + => _container.DisposeAsync().AsTask(); public Task InitializeAsync() - { - return _container.StartAsync(); - } + => _container.StartAsync(); } [CollectionDefinition(Name)] diff --git a/tests/SquidStd.Tests/Messaging/RabbitMq/RabbitMqQueueProviderTests.cs b/tests/SquidStd.Tests/Messaging/RabbitMq/RabbitMqQueueProviderTests.cs index ae74710c..b55e5de2 100644 --- a/tests/SquidStd.Tests/Messaging/RabbitMq/RabbitMqQueueProviderTests.cs +++ b/tests/SquidStd.Tests/Messaging/RabbitMq/RabbitMqQueueProviderTests.cs @@ -1,6 +1,5 @@ using System.Text; using SquidStd.Messaging.Abstractions.Data.Config; -using SquidStd.Messaging.RabbitMq.Data.Config; using SquidStd.Messaging.RabbitMq.Services; namespace SquidStd.Tests.Messaging.RabbitMq; @@ -20,7 +19,7 @@ public RabbitMqQueueProviderTests(RabbitMqContainerFixture fixture) [Fact] public async Task AlwaysFailing_IsDeadLettered() { - await using var provider = NewProvider(new MessagingOptions { MaxDeliveryAttempts = 2 }); + await using var provider = NewProvider(new() { MaxDeliveryAttempts = 2 }); await provider.StartAsync(); var queue = Queue(); provider.Subscribe(queue, (_, _) => throw new InvalidOperationException("always")); @@ -106,25 +105,17 @@ public async Task TwoSubscribers_ShareTheLoad() } private static ReadOnlyMemory Bytes(string s) - { - return Encoding.UTF8.GetBytes(s); - } + => Encoding.UTF8.GetBytes(s); private RabbitMqQueueProvider NewProvider(MessagingOptions? options = null) - { - return new RabbitMqQueueProvider( - new RabbitMqOptions { Uri = new Uri(_fixture.AmqpUri) }, + => new( + new() { Uri = new(_fixture.AmqpUri) }, options ?? new MessagingOptions() ); - } private static string Queue() - { - return "q-" + Guid.NewGuid().ToString("N"); - } + => "q-" + Guid.NewGuid().ToString("N"); private static string Text(ReadOnlyMemory b) - { - return Encoding.UTF8.GetString(b.Span); - } + => Encoding.UTF8.GetString(b.Span); } diff --git a/tests/SquidStd.Tests/Messaging/RabbitMq/RabbitMqTopicProviderTests.cs b/tests/SquidStd.Tests/Messaging/RabbitMq/RabbitMqTopicProviderTests.cs index 23691198..7cfbdffe 100644 --- a/tests/SquidStd.Tests/Messaging/RabbitMq/RabbitMqTopicProviderTests.cs +++ b/tests/SquidStd.Tests/Messaging/RabbitMq/RabbitMqTopicProviderTests.cs @@ -1,5 +1,4 @@ using System.Text; -using SquidStd.Messaging.RabbitMq.Data.Config; using SquidStd.Messaging.RabbitMq.Services; namespace SquidStd.Tests.Messaging.RabbitMq; @@ -76,17 +75,11 @@ public async Task Publish_FansOutToAllSubscribers() } private static ReadOnlyMemory Bytes(string s) - { - return Encoding.UTF8.GetBytes(s); - } + => Encoding.UTF8.GetBytes(s); private RabbitMqTopicProvider NewProvider() - { - return new RabbitMqTopicProvider(new RabbitMqOptions { Uri = new Uri(_fixture.AmqpUri) }); - } + => new(new() { Uri = new(_fixture.AmqpUri) }); private static string Topic() - { - return "topic-" + Guid.NewGuid().ToString("N"); - } + => "topic-" + Guid.NewGuid().ToString("N"); } diff --git a/tests/SquidStd.Tests/Messaging/Sqs/LocalStackContainerFixture.cs b/tests/SquidStd.Tests/Messaging/Sqs/LocalStackContainerFixture.cs index 98b86d4e..c67be6c3 100644 --- a/tests/SquidStd.Tests/Messaging/Sqs/LocalStackContainerFixture.cs +++ b/tests/SquidStd.Tests/Messaging/Sqs/LocalStackContainerFixture.cs @@ -4,31 +4,28 @@ namespace SquidStd.Tests.Messaging.Sqs; /// -/// Starts a LocalStack container (SQS + SNS) once for the whole collection and exposes an -/// pointing at its edge endpoint with dummy credentials. +/// Starts a LocalStack container (SQS + SNS) once for the whole collection and exposes an +/// pointing at its edge endpoint with dummy credentials. /// public sealed class LocalStackContainerFixture : IAsyncLifetime { private readonly LocalStackContainer _container = new LocalStackBuilder().WithImage("localstack/localstack:3").Build(); - public AwsConfigEntry Aws => new() - { - Region = "us-east-1", - AccessKey = "test", - SecretKey = "test", - ServiceUrl = _container.GetConnectionString() - }; + public AwsConfigEntry Aws + => new() + { + Region = "us-east-1", + AccessKey = "test", + SecretKey = "test", + ServiceUrl = _container.GetConnectionString() + }; public Task DisposeAsync() - { - return _container.DisposeAsync().AsTask(); - } + => _container.DisposeAsync().AsTask(); public Task InitializeAsync() - { - return _container.StartAsync(); - } + => _container.StartAsync(); } [CollectionDefinition(Name)] diff --git a/tests/SquidStd.Tests/Messaging/Sqs/SqsConnectionStringTests.cs b/tests/SquidStd.Tests/Messaging/Sqs/SqsConnectionStringTests.cs index 00b83af0..032978c8 100644 --- a/tests/SquidStd.Tests/Messaging/Sqs/SqsConnectionStringTests.cs +++ b/tests/SquidStd.Tests/Messaging/Sqs/SqsConnectionStringTests.cs @@ -36,9 +36,7 @@ public void Parse_WithoutCredentials_LeavesThemNull() [Fact] public void WrongScheme_Throws() - { - Assert.Throws(() => SqsMessagingRegistrationExtensions.ParseOptions("rabbitmq://x")); - } + => Assert.Throws(() => SqsMessagingRegistrationExtensions.ParseOptions("rabbitmq://x")); [Fact] public void AddSqsMessaging_RegistersBothProviders() diff --git a/tests/SquidStd.Tests/Messaging/Sqs/SqsNamesTests.cs b/tests/SquidStd.Tests/Messaging/Sqs/SqsNamesTests.cs index 47779db9..31b555ab 100644 --- a/tests/SquidStd.Tests/Messaging/Sqs/SqsNamesTests.cs +++ b/tests/SquidStd.Tests/Messaging/Sqs/SqsNamesTests.cs @@ -6,19 +6,13 @@ public class SqsNamesTests { [Fact] public void Sanitize_ReplacesDotsWithDashes() - { - Assert.Equal("orders-dlq", SqsNames.Sanitize("orders.dlq")); - } + => Assert.Equal("orders-dlq", SqsNames.Sanitize("orders.dlq")); [Fact] public void Sanitize_KeepsLettersDigitsDashUnderscore() - { - Assert.Equal("a_b-9", SqsNames.Sanitize("a_b-9")); - } + => Assert.Equal("a_b-9", SqsNames.Sanitize("a_b-9")); [Fact] public void Sanitize_ReplacesSlashesAndColons() - { - Assert.Equal("a-b-c", SqsNames.Sanitize("a/b:c")); - } + => Assert.Equal("a-b-c", SqsNames.Sanitize("a/b:c")); } diff --git a/tests/SquidStd.Tests/Messaging/Sqs/SqsQueueProviderTests.cs b/tests/SquidStd.Tests/Messaging/Sqs/SqsQueueProviderTests.cs index 073a175f..6eab7439 100644 --- a/tests/SquidStd.Tests/Messaging/Sqs/SqsQueueProviderTests.cs +++ b/tests/SquidStd.Tests/Messaging/Sqs/SqsQueueProviderTests.cs @@ -55,6 +55,7 @@ public async Task TwoSubscribers_ShareTheLoad() { Interlocked.Increment(ref a); done.Signal(); + return Task.CompletedTask; } ); @@ -64,6 +65,7 @@ public async Task TwoSubscribers_ShareTheLoad() { Interlocked.Increment(ref b); done.Signal(); + return Task.CompletedTask; } ); @@ -81,8 +83,8 @@ public async Task TwoSubscribers_ShareTheLoad() public async Task AlwaysFailing_IsDeadLettered() { await using var provider = NewProvider( - new MessagingOptions { MaxDeliveryAttempts = 1 }, - new SqsOptions { Aws = _fixture.Aws, VisibilityTimeout = TimeSpan.FromSeconds(1), WaitTimeSeconds = 1 } + new() { MaxDeliveryAttempts = 1 }, + new() { Aws = _fixture.Aws, VisibilityTimeout = TimeSpan.FromSeconds(1), WaitTimeSeconds = 1 } ); await provider.StartAsync(); var queue = Queue(); @@ -94,6 +96,7 @@ public async Task AlwaysFailing_IsDeadLettered() (payload, _) => { deadLettered.TrySetResult(Text(payload)); + return Task.CompletedTask; } ); @@ -104,25 +107,17 @@ public async Task AlwaysFailing_IsDeadLettered() } private SqsQueueProvider NewProvider(MessagingOptions? messaging = null, SqsOptions? sqs = null) - { - return new SqsQueueProvider( + => new( sqs ?? new SqsOptions { Aws = _fixture.Aws, WaitTimeSeconds = 1 }, messaging ?? new MessagingOptions() ); - } private static ReadOnlyMemory Bytes(string s) - { - return Encoding.UTF8.GetBytes(s); - } + => Encoding.UTF8.GetBytes(s); private static string Text(ReadOnlyMemory b) - { - return Encoding.UTF8.GetString(b.Span); - } + => Encoding.UTF8.GetString(b.Span); private static string Queue() - { - return "q-" + Guid.NewGuid().ToString("N"); - } + => "q-" + Guid.NewGuid().ToString("N"); } diff --git a/tests/SquidStd.Tests/Messaging/Sqs/SqsTopicProviderTests.cs b/tests/SquidStd.Tests/Messaging/Sqs/SqsTopicProviderTests.cs index 818df2ea..5a591d99 100644 --- a/tests/SquidStd.Tests/Messaging/Sqs/SqsTopicProviderTests.cs +++ b/tests/SquidStd.Tests/Messaging/Sqs/SqsTopicProviderTests.cs @@ -1,5 +1,4 @@ using System.Text; -using SquidStd.Messaging.Sqs.Data.Config; using SquidStd.Messaging.Sqs.Services; namespace SquidStd.Tests.Messaging.Sqs; @@ -29,6 +28,7 @@ public async Task Publish_FansOutToAllSubscribers() (payload, _) => { a.TrySetResult(Text(payload)); + return Task.CompletedTask; } ); @@ -37,6 +37,7 @@ public async Task Publish_FansOutToAllSubscribers() (payload, _) => { b.TrySetResult(Text(payload)); + return Task.CompletedTask; } ); @@ -50,22 +51,14 @@ public async Task Publish_FansOutToAllSubscribers() } private SqsTopicProvider NewProvider() - { - return new SqsTopicProvider(new SqsOptions { Aws = _fixture.Aws, WaitTimeSeconds = 1 }); - } + => new(new() { Aws = _fixture.Aws, WaitTimeSeconds = 1 }); private static ReadOnlyMemory Bytes(string s) - { - return Encoding.UTF8.GetBytes(s); - } + => Encoding.UTF8.GetBytes(s); private static string Text(ReadOnlyMemory b) - { - return Encoding.UTF8.GetString(b.Span); - } + => Encoding.UTF8.GetString(b.Span); private static string Topic() - { - return "topic-" + Guid.NewGuid().ToString("N"); - } + => "topic-" + Guid.NewGuid().ToString("N"); } diff --git a/tests/SquidStd.Tests/Network/CircularBufferTests.cs b/tests/SquidStd.Tests/Network/CircularBufferTests.cs index 70306fe9..8b1215db 100644 --- a/tests/SquidStd.Tests/Network/CircularBufferTests.cs +++ b/tests/SquidStd.Tests/Network/CircularBufferTests.cs @@ -18,15 +18,11 @@ public void Clear_ResetsSizeButKeepsCapacity() [Fact] public void Constructor_NonPositiveCapacity_Throws() - { - Assert.Throws(() => new CircularBuffer(0)); - } + => Assert.Throws(() => new CircularBuffer(0)); [Fact] public void Constructor_TooManyItems_Throws() - { - Assert.Throws(() => new CircularBuffer(2, [1, 2, 3])); - } + => Assert.Throws(() => new CircularBuffer(2, [1, 2, 3])); [Fact] public void Constructor_WithItems_PopulatesBuffer() diff --git a/tests/SquidStd.Tests/Network/NetMiddlewarePipelineTests.cs b/tests/SquidStd.Tests/Network/NetMiddlewarePipelineTests.cs index 2598633e..01657a69 100644 --- a/tests/SquidStd.Tests/Network/NetMiddlewarePipelineTests.cs +++ b/tests/SquidStd.Tests/Network/NetMiddlewarePipelineTests.cs @@ -30,8 +30,9 @@ public async Task ExecuteAsync_CancelledToken_Throws() using var cts = new CancellationTokenSource(); await cts.CancelAsync(); - await Assert.ThrowsAsync(async () => - await pipeline.ExecuteAsync(null, new byte[] { 1 }, cts.Token) + await Assert.ThrowsAsync( + async () => + await pipeline.ExecuteAsync(null, new byte[] { 1 }, cts.Token) ); } diff --git a/tests/SquidStd.Tests/Network/PacketExtensionsTests.cs b/tests/SquidStd.Tests/Network/PacketExtensionsTests.cs index 96099258..5f680b54 100644 --- a/tests/SquidStd.Tests/Network/PacketExtensionsTests.cs +++ b/tests/SquidStd.Tests/Network/PacketExtensionsTests.cs @@ -4,13 +4,7 @@ namespace SquidStd.Tests.Network; public class PacketExtensionsTests { - [Theory] - [InlineData(0x00, "0x00")] - [InlineData(0x0F, "0x0F")] - [InlineData(0xAB, "0xAB")] - [InlineData(0xFF, "0xFF")] + [Theory, InlineData(0x00, "0x00"), InlineData(0x0F, "0x0F"), InlineData(0xAB, "0xAB"), InlineData(0xFF, "0xFF")] public void ToPacketString_FormatsAsTwoDigitHex(byte opCode, string expected) - { - Assert.Equal(expected, opCode.ToPacketString()); - } + => Assert.Equal(expected, opCode.ToPacketString()); } diff --git a/tests/SquidStd.Tests/Network/SessionManagerTests.cs b/tests/SquidStd.Tests/Network/SessionManagerTests.cs index 54084230..7032ef30 100644 --- a/tests/SquidStd.Tests/Network/SessionManagerTests.cs +++ b/tests/SquidStd.Tests/Network/SessionManagerTests.cs @@ -139,7 +139,7 @@ public async Task Integration_LifecycleOverLoopback() { var timeout = TimeSpan.FromSeconds(5); - await using var server = new SquidTcpServer(new IPEndPoint(IPAddress.Loopback, 0)); + await using var server = new SquidTcpServer(new(IPAddress.Loopback, 0)); using var manager = NewManager(server); var created = new TaskCompletionSource>(TaskCreationOptions.RunContinuationsAsynchronously); @@ -152,7 +152,7 @@ public async Task Integration_LifecycleOverLoopback() await server.StartAsync(CancellationToken.None); var port = server.Port; - var client = await SquidStdTcpClient.ConnectAsync(new IPEndPoint(IPAddress.Loopback, port)); + var client = await SquidStdTcpClient.ConnectAsync(new(IPAddress.Loopback, port)); var session = await created.Task.WaitAsync(timeout); Assert.Equal(1, manager.Count); @@ -216,12 +216,8 @@ public void TryGetSession_HitAndMiss() } private static SessionManager NewManager(SquidTcpServer server) - { - return new SessionManager(server, connection => $"state-{connection.SessionId}"); - } + => new(server, connection => $"state-{connection.SessionId}"); private static SquidTcpServer NewServer() - { - return new SquidTcpServer(new IPEndPoint(IPAddress.Loopback, 0)); - } + => new(new(IPAddress.Loopback, 0)); } diff --git a/tests/SquidStd.Tests/Network/SquidStdUdpClientTests.cs b/tests/SquidStd.Tests/Network/SquidStdUdpClientTests.cs index f80106d8..334d8ffb 100644 --- a/tests/SquidStd.Tests/Network/SquidStdUdpClientTests.cs +++ b/tests/SquidStd.Tests/Network/SquidStdUdpClientTests.cs @@ -10,7 +10,7 @@ public class SquidStdUdpClientTests [Fact] public async Task CloseAsync_RaisesDisconnectedOnceAndMarksDisconnected() { - var client = new SquidStdUdpClient(new IPEndPoint(IPAddress.Loopback, 0)); + var client = new SquidStdUdpClient(new(IPAddress.Loopback, 0)); var disconnects = 0; client.OnDisconnected += (_, _) => Interlocked.Increment(ref disconnects); await client.StartAsync(CancellationToken.None); @@ -25,8 +25,8 @@ public async Task CloseAsync_RaisesDisconnectedOnceAndMarksDisconnected() [Fact] public void Constructor_AssignsUniqueSessionIds() { - using var first = new SquidStdUdpClient(new IPEndPoint(IPAddress.Loopback, 0)); - using var second = new SquidStdUdpClient(new IPEndPoint(IPAddress.Loopback, 0)); + using var first = new SquidStdUdpClient(new(IPAddress.Loopback, 0)); + using var second = new SquidStdUdpClient(new(IPAddress.Loopback, 0)); Assert.NotEqual(first.SessionId, second.SessionId); } @@ -34,7 +34,7 @@ public void Constructor_AssignsUniqueSessionIds() [Fact] public void Constructor_BindsLocalEndPoint() { - using var client = new SquidStdUdpClient(new IPEndPoint(IPAddress.Loopback, 0)); + using var client = new SquidStdUdpClient(new(IPAddress.Loopback, 0)); var local = Assert.IsType(client.LocalEndPoint); Assert.NotEqual(0, local.Port); @@ -46,7 +46,7 @@ public void RemoteEndPoint_ReflectsConfiguredDefault() { var remote = new IPEndPoint(IPAddress.Loopback, 9999); - using var client = new SquidStdUdpClient(new IPEndPoint(IPAddress.Loopback, 0), remote); + using var client = new SquidStdUdpClient(new(IPAddress.Loopback, 0), remote); Assert.Equal(remote, client.RemoteEndPoint); } @@ -54,7 +54,7 @@ public void RemoteEndPoint_ReflectsConfiguredDefault() [Fact] public async Task SendAsync_UsesConfiguredDefaultRemote() { - await using var receiver = new SquidStdUdpClient(new IPEndPoint(IPAddress.Loopback, 0)); + await using var receiver = new SquidStdUdpClient(new(IPAddress.Loopback, 0)); var received = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); receiver.OnDataReceived += (_, e) => received.TrySetResult(e.Data.ToArray()); await receiver.StartAsync(CancellationToken.None); @@ -62,8 +62,8 @@ public async Task SendAsync_UsesConfiguredDefaultRemote() var receiverPort = ((IPEndPoint)receiver.LocalEndPoint!).Port; await using var sender = new SquidStdUdpClient( - new IPEndPoint(IPAddress.Loopback, 0), - new IPEndPoint(IPAddress.Loopback, receiverPort) + new(IPAddress.Loopback, 0), + new(IPAddress.Loopback, receiverPort) ); await sender.StartAsync(CancellationToken.None); await sender.SendAsync(new byte[] { 9, 8, 7 }, CancellationToken.None); @@ -75,28 +75,29 @@ public async Task SendAsync_UsesConfiguredDefaultRemote() [Fact] public async Task SendAsync_WithoutDefaultRemote_Throws() { - await using var client = new SquidStdUdpClient(new IPEndPoint(IPAddress.Loopback, 0)); + await using var client = new SquidStdUdpClient(new(IPAddress.Loopback, 0)); - await Assert.ThrowsAsync(async () => - await client.SendAsync(new byte[] { 1 }, CancellationToken.None) + await Assert.ThrowsAsync( + async () => + await client.SendAsync(new byte[] { 1 }, CancellationToken.None) ); } [Fact] public async Task SendToAsync_DeliversDatagramToPeer() { - await using var receiver = new SquidStdUdpClient(new IPEndPoint(IPAddress.Loopback, 0)); + await using var receiver = new SquidStdUdpClient(new(IPAddress.Loopback, 0)); var received = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); receiver.OnDataReceived += (_, e) => received.TrySetResult(e.Data.ToArray()); await receiver.StartAsync(CancellationToken.None); var receiverPort = ((IPEndPoint)receiver.LocalEndPoint!).Port; - await using var sender = new SquidStdUdpClient(new IPEndPoint(IPAddress.Loopback, 0)); + await using var sender = new SquidStdUdpClient(new(IPAddress.Loopback, 0)); await sender.StartAsync(CancellationToken.None); await sender.SendToAsync( new byte[] { 1, 2, 3, 4 }, - new IPEndPoint(IPAddress.Loopback, receiverPort), + new(IPAddress.Loopback, receiverPort), CancellationToken.None ); @@ -107,7 +108,7 @@ await sender.SendToAsync( [Fact] public async Task StartAsync_RaisesConnected() { - await using var client = new SquidStdUdpClient(new IPEndPoint(IPAddress.Loopback, 0)); + await using var client = new SquidStdUdpClient(new(IPAddress.Loopback, 0)); var connected = false; client.OnConnected += (_, _) => connected = true; diff --git a/tests/SquidStd.Tests/Network/SquidStdUdpServerTests.cs b/tests/SquidStd.Tests/Network/SquidStdUdpServerTests.cs index 2f9d4d5d..de740c6f 100644 --- a/tests/SquidStd.Tests/Network/SquidStdUdpServerTests.cs +++ b/tests/SquidStd.Tests/Network/SquidStdUdpServerTests.cs @@ -12,7 +12,7 @@ public class SquidStdUdpServerTests [Fact] public async Task BindSingleInterface_HasOneListener() { - await using var server = new SquidStdUdpServer(new IPEndPoint(IPAddress.Loopback, 0), false); + await using var server = new SquidStdUdpServer(new(IPAddress.Loopback, 0), false); await server.StartAsync(CancellationToken.None); Assert.Equal(1, server.ListenerCount); @@ -21,17 +21,17 @@ public async Task BindSingleInterface_HasOneListener() [Fact] public async Task DefaultBehaviour_EchoesDatagramBackToSender() { - await using var server = new SquidStdUdpServer(new IPEndPoint(IPAddress.Loopback, 0), false); + await using var server = new SquidStdUdpServer(new(IPAddress.Loopback, 0), false); await server.StartAsync(CancellationToken.None); var serverPort = server.Port; - await using var client = new SquidStdUdpClient(new IPEndPoint(IPAddress.Loopback, 0)); + await using var client = new SquidStdUdpClient(new(IPAddress.Loopback, 0)); var received = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); client.OnDataReceived += (_, e) => received.TrySetResult(e.Data.ToArray()); await client.StartAsync(CancellationToken.None); var payload = new byte[] { 1, 2, 3, 4, 5 }; - await client.SendToAsync(payload, new IPEndPoint(IPAddress.Loopback, serverPort), CancellationToken.None); + await client.SendToAsync(payload, new(IPAddress.Loopback, serverPort), CancellationToken.None); Assert.Equal(payload, await received.Task.WaitAsync(Timeout)); } @@ -39,19 +39,19 @@ public async Task DefaultBehaviour_EchoesDatagramBackToSender() [Fact] public async Task OnDatagram_CustomResponse_IsReturnedToSender() { - await using var server = new SquidStdUdpServer(new IPEndPoint(IPAddress.Loopback, 0), false) + await using var server = new SquidStdUdpServer(new(IPAddress.Loopback, 0), false) { OnDatagram = (_, _) => new byte[] { 0xFF, 0xFE } }; await server.StartAsync(CancellationToken.None); var serverPort = server.Port; - await using var client = new SquidStdUdpClient(new IPEndPoint(IPAddress.Loopback, 0)); + await using var client = new SquidStdUdpClient(new(IPAddress.Loopback, 0)); var received = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); client.OnDataReceived += (_, e) => received.TrySetResult(e.Data.ToArray()); await client.StartAsync(CancellationToken.None); - await client.SendToAsync(new byte[] { 1 }, new IPEndPoint(IPAddress.Loopback, serverPort), CancellationToken.None); + await client.SendToAsync(new byte[] { 1 }, new(IPAddress.Loopback, serverPort), CancellationToken.None); Assert.Equal([0xFF, 0xFE], await received.Task.WaitAsync(Timeout)); } @@ -59,17 +59,17 @@ public async Task OnDatagram_CustomResponse_IsReturnedToSender() [Fact] public async Task OnDatagramReceived_RaisedForIncomingDatagram() { - await using var server = new SquidStdUdpServer(new IPEndPoint(IPAddress.Loopback, 0), false); + await using var server = new SquidStdUdpServer(new(IPAddress.Loopback, 0), false); var received = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); server.OnDatagramReceived += (_, e) => received.TrySetResult(e.Data.ToArray()); await server.StartAsync(CancellationToken.None); var serverPort = server.Port; - await using var client = new SquidStdUdpClient(new IPEndPoint(IPAddress.Loopback, 0)); + await using var client = new SquidStdUdpClient(new(IPAddress.Loopback, 0)); await client.StartAsync(CancellationToken.None); await client.SendToAsync( new byte[] { 1, 2, 3 }, - new IPEndPoint(IPAddress.Loopback, serverPort), + new(IPAddress.Loopback, serverPort), CancellationToken.None ); @@ -79,7 +79,7 @@ await client.SendToAsync( [Fact] public async Task SendToAsync_DeliversToEndpointThatWasSeen() { - await using var server = new SquidStdUdpServer(new IPEndPoint(IPAddress.Loopback, 0), false) + await using var server = new SquidStdUdpServer(new(IPAddress.Loopback, 0), false) { OnDatagram = static (_, _) => ReadOnlyMemory.Empty // suppress default echo for this test }; @@ -88,13 +88,13 @@ public async Task SendToAsync_DeliversToEndpointThatWasSeen() await server.StartAsync(CancellationToken.None); var serverPort = server.Port; - await using var client = new SquidStdUdpClient(new IPEndPoint(IPAddress.Loopback, 0)); + await using var client = new SquidStdUdpClient(new(IPAddress.Loopback, 0)); var clientReceived = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); client.OnDataReceived += (_, e) => clientReceived.TrySetResult(e.Data.ToArray()); await client.StartAsync(CancellationToken.None); // Make the server "see" the client endpoint first. - await client.SendToAsync(new byte[] { 0 }, new IPEndPoint(IPAddress.Loopback, serverPort), CancellationToken.None); + await client.SendToAsync(new byte[] { 0 }, new(IPAddress.Loopback, serverPort), CancellationToken.None); var clientEndpoint = await senderEndpoint.Task.WaitAsync(Timeout); await server.SendToAsync(clientEndpoint, new byte[] { 9, 9 }, CancellationToken.None); @@ -105,7 +105,7 @@ public async Task SendToAsync_DeliversToEndpointThatWasSeen() [Fact] public void ServerType_IsUdp() { - using var server = new SquidStdUdpServer(new IPEndPoint(IPAddress.Loopback, 0), false); + using var server = new SquidStdUdpServer(new(IPAddress.Loopback, 0), false); Assert.Equal(ServerType.UDP, server.ServerType); } @@ -113,7 +113,7 @@ public void ServerType_IsUdp() [Fact] public async Task StartStop_TogglesIsRunning() { - await using var server = new SquidStdUdpServer(new IPEndPoint(IPAddress.Loopback, 0), false); + await using var server = new SquidStdUdpServer(new(IPAddress.Loopback, 0), false); Assert.False(server.IsRunning); await server.StartAsync(CancellationToken.None); diff --git a/tests/SquidStd.Tests/Network/TcpMaxFrameLengthTests.cs b/tests/SquidStd.Tests/Network/TcpMaxFrameLengthTests.cs index 7ad61377..91203987 100644 --- a/tests/SquidStd.Tests/Network/TcpMaxFrameLengthTests.cs +++ b/tests/SquidStd.Tests/Network/TcpMaxFrameLengthTests.cs @@ -8,36 +8,10 @@ namespace SquidStd.Tests.Network; public sealed class TcpMaxFrameLengthTests { - // 4-byte big-endian length prefix framer. - private sealed class LengthPrefixFramer : INetFramer - { - public bool TryReadFrame(ReadOnlySpan buffer, out int frameLength) - { - frameLength = 0; - - if (buffer.Length < 4) - { - return false; - } - - var payloadLength = BinaryPrimitives.ReadInt32BigEndian(buffer); - var total = 4 + payloadLength; - - if (buffer.Length < total) - { - return false; - } - - frameLength = total; - - return true; - } - } - [Fact] public async Task OversizedDeclaredFrame_ClosesConnection() { - var (server, client) = await ConnectedPairAsync(maxFrameLength: 1024); + var (server, client) = await ConnectedPairAsync(1024); try { @@ -61,7 +35,7 @@ public async Task OversizedDeclaredFrame_ClosesConnection() [Fact] public async Task FrameAtTheLimit_IsDelivered() { - var (server, client) = await ConnectedPairAsync(maxFrameLength: 1024); + var (server, client) = await ConnectedPairAsync(1024); var received = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); client.OnDataReceived += (_, e) => received.TrySetResult(e.Data.ToArray()); @@ -99,8 +73,13 @@ public async Task FrameAtTheLimit_IsDelivered() // "server" side here is just the sending end; the receiving end (client) enforces the cap. var server = new SquidStdTcpClient(serverSocket); - var client = new SquidStdTcpClient(clientSocket, middlewares: null, framer: new LengthPrefixFramer(), - codec: null, maxFrameLength: maxFrameLength); + var client = new SquidStdTcpClient( + clientSocket, + middlewares: null, + new LengthPrefixFramer(), + null, + maxFrameLength: maxFrameLength + ); await server.StartAsync(CancellationToken.None); await client.StartAsync(CancellationToken.None); @@ -124,4 +103,30 @@ private static async Task WaitUntilAsync(Func condition, TimeSpan ti return condition(); } + + // 4-byte big-endian length prefix framer. + private sealed class LengthPrefixFramer : INetFramer + { + public bool TryReadFrame(ReadOnlySpan buffer, out int frameLength) + { + frameLength = 0; + + if (buffer.Length < 4) + { + return false; + } + + var payloadLength = BinaryPrimitives.ReadInt32BigEndian(buffer); + var total = 4 + payloadLength; + + if (buffer.Length < total) + { + return false; + } + + frameLength = total; + + return true; + } + } } diff --git a/tests/SquidStd.Tests/Network/TransportCodecTests.cs b/tests/SquidStd.Tests/Network/TransportCodecTests.cs index fe812271..c02f6cf5 100644 --- a/tests/SquidStd.Tests/Network/TransportCodecTests.cs +++ b/tests/SquidStd.Tests/Network/TransportCodecTests.cs @@ -1,7 +1,6 @@ using System.Collections.Concurrent; using System.Net; using SquidStd.Network.Client; -using SquidStd.Network.Data; using SquidStd.Network.Server; using SquidStd.Tests.Support; @@ -17,16 +16,16 @@ public async Task Codec_RoundTripsClientToServer() var received = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); await using var server = new SquidTcpServer( - new IPEndPoint(IPAddress.Loopback, 0), - connectionPipelineFactory: () => new ConnectionPipeline(new CountingXorCodec(7)) + new(IPAddress.Loopback, 0), + connectionPipelineFactory: () => new(new CountingXorCodec(7)) ); server.OnDataReceived += (_, e) => received.TrySetResult(e.Data.ToArray()); await server.StartAsync(CancellationToken.None); await using var client = await SquidStdTcpClient.ConnectAsync( - new IPEndPoint(IPAddress.Loopback, server.Port), - codec: new CountingXorCodec(7) - ); + new(IPAddress.Loopback, server.Port), + codec: new CountingXorCodec(7) + ); var payload = new byte[] { 1, 2, 3, 4, 5 }; await client.SendAsync(payload, CancellationToken.None); @@ -39,11 +38,11 @@ public async Task NoCodecNoFactory_PassesBytesUnchanged() { var received = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - await using var server = new SquidTcpServer(new IPEndPoint(IPAddress.Loopback, 0)); + await using var server = new SquidTcpServer(new(IPAddress.Loopback, 0)); server.OnDataReceived += (_, e) => received.TrySetResult(e.Data.ToArray()); await server.StartAsync(CancellationToken.None); - await using var client = await SquidStdTcpClient.ConnectAsync(new IPEndPoint(IPAddress.Loopback, server.Port)); + await using var client = await SquidStdTcpClient.ConnectAsync(new(IPAddress.Loopback, server.Port)); var payload = new byte[] { 42, 43, 44 }; await client.SendAsync(payload, CancellationToken.None); @@ -62,20 +61,20 @@ public async Task Codec_ConcurrentSends_PreserveKeystreamIntegrity() using var done = new CountdownEvent(messageCount); await using var server = new SquidTcpServer( - new IPEndPoint(IPAddress.Loopback, 0), - connectionPipelineFactory: () => new ConnectionPipeline(new CountingXorCodec(5), null, new LengthPrefixFramer()) + new(IPAddress.Loopback, 0), + connectionPipelineFactory: () => new(new CountingXorCodec(5), null, new LengthPrefixFramer()) ); server.OnDataReceived += (_, e) => - { - frames.Add(e.Data.ToArray()); - done.Signal(); - }; + { + frames.Add(e.Data.ToArray()); + done.Signal(); + }; await server.StartAsync(CancellationToken.None); await using var client = await SquidStdTcpClient.ConnectAsync( - new IPEndPoint(IPAddress.Loopback, server.Port), - codec: new CountingXorCodec(5) - ); + new(IPAddress.Loopback, server.Port), + codec: new CountingXorCodec(5) + ); await Parallel.ForEachAsync( Enumerable.Range(0, messageCount), @@ -121,16 +120,16 @@ public async Task Codec_WithFramer_EmitsDecodedFrames() var frames = new BlockingCollection(); await using var server = new SquidTcpServer( - new IPEndPoint(IPAddress.Loopback, 0), - connectionPipelineFactory: () => new ConnectionPipeline(new CountingXorCodec(2), null, new LengthPrefixFramer()) + new(IPAddress.Loopback, 0), + connectionPipelineFactory: () => new(new CountingXorCodec(2), null, new LengthPrefixFramer()) ); server.OnDataReceived += (_, e) => frames.Add(e.Data.ToArray()); await server.StartAsync(CancellationToken.None); await using var client = await SquidStdTcpClient.ConnectAsync( - new IPEndPoint(IPAddress.Loopback, server.Port), - codec: new CountingXorCodec(2) - ); + new(IPAddress.Loopback, server.Port), + codec: new CountingXorCodec(2) + ); // Three length-prefixed messages in a single send: [len=2][AA BB] [len=1][CC] [len=3][01 02 03]. var buffer = new byte[] { 2, 0xAA, 0xBB, 1, 0xCC, 3, 0x01, 0x02, 0x03 }; @@ -151,8 +150,8 @@ public async Task Codec_IsolatesStatePerConnection() var inbox = new BlockingCollection(); await using var server = new SquidTcpServer( - new IPEndPoint(IPAddress.Loopback, 0), - connectionPipelineFactory: () => new ConnectionPipeline(new CountingXorCodec(3)) + new(IPAddress.Loopback, 0), + connectionPipelineFactory: () => new(new CountingXorCodec(3)) ); server.OnDataReceived += (_, e) => inbox.Add(e.Data.ToArray()); await server.StartAsync(CancellationToken.None); @@ -162,9 +161,9 @@ public async Task Codec_IsolatesStatePerConnection() for (var i = 0; i < 2; i++) { await using var client = await SquidStdTcpClient.ConnectAsync( - new IPEndPoint(IPAddress.Loopback, server.Port), - codec: new CountingXorCodec(3) - ); + new(IPAddress.Loopback, server.Port), + codec: new CountingXorCodec(3) + ); await client.SendAsync(payload, CancellationToken.None); Assert.True(inbox.TryTake(out var got, Timeout)); @@ -181,27 +180,27 @@ public async Task Codec_SwapsAtomicallyMidConnection() var second = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); await using var server = new SquidTcpServer( - new IPEndPoint(IPAddress.Loopback, 0), - connectionPipelineFactory: () => new ConnectionPipeline(new CountingXorCodec(10)) + new(IPAddress.Loopback, 0), + connectionPipelineFactory: () => new(new CountingXorCodec(10)) ); server.OnDataReceived += (_, e) => - { - if (first.Task.IsCompleted) - { - second.TrySetResult(e.Data.ToArray()); - } - else - { - e.Client.SwapCodec(new CountingXorCodec(20)); - first.TrySetResult(e.Data.ToArray()); - } - }; + { + if (first.Task.IsCompleted) + { + second.TrySetResult(e.Data.ToArray()); + } + else + { + e.Client.SwapCodec(new CountingXorCodec(20)); + first.TrySetResult(e.Data.ToArray()); + } + }; await server.StartAsync(CancellationToken.None); await using var client = await SquidStdTcpClient.ConnectAsync( - new IPEndPoint(IPAddress.Loopback, server.Port), - codec: new CountingXorCodec(10) - ); + new(IPAddress.Loopback, server.Port), + codec: new CountingXorCodec(10) + ); await client.SendAsync(msg1, CancellationToken.None); Assert.Equal(msg1, await first.Task.WaitAsync(Timeout)); diff --git a/tests/SquidStd.Tests/Network/UdpSessionManagerTests.cs b/tests/SquidStd.Tests/Network/UdpSessionManagerTests.cs index 5980026e..2dfaf20f 100644 --- a/tests/SquidStd.Tests/Network/UdpSessionManagerTests.cs +++ b/tests/SquidStd.Tests/Network/UdpSessionManagerTests.cs @@ -82,7 +82,7 @@ public async Task Integration_DatagramCreatesSessionAndManagerCanReply() { var timeout = TimeSpan.FromSeconds(5); - await using var server = new SquidStdUdpServer(new IPEndPoint(IPAddress.Loopback, 0), false); + await using var server = new SquidStdUdpServer(new(IPAddress.Loopback, 0), false); using var manager = new UdpSessionManager(server, c => $"state-{c.SessionId}"); var created = new TaskCompletionSource>(TaskCreationOptions.RunContinuationsAsynchronously); @@ -93,14 +93,14 @@ public async Task Integration_DatagramCreatesSessionAndManagerCanReply() await server.StartAsync(CancellationToken.None); var serverPort = server.Port; - await using var client = new SquidStdUdpClient(new IPEndPoint(IPAddress.Loopback, 0)); + await using var client = new SquidStdUdpClient(new(IPAddress.Loopback, 0)); var clientReceived = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); client.OnDataReceived += (_, e) => clientReceived.TrySetResult(e.Data.ToArray()); await client.StartAsync(CancellationToken.None); await client.SendToAsync( new byte[] { 1, 2, 3 }, - new IPEndPoint(IPAddress.Loopback, serverPort), + new(IPAddress.Loopback, serverPort), CancellationToken.None ); @@ -168,23 +168,17 @@ public void SweepExpiredSessions_RemovesIdleSessions() } private static UdpSessionManager NewManager(SquidStdUdpServer server, FakeTimeProvider time) - { - return new UdpSessionManager( + => new( server, connection => $"state-{connection.SessionId}", TimeSpan.FromSeconds(30), TimeSpan.FromSeconds(10), time ); - } private static SquidStdUdpServer NewServer() - { - return new SquidStdUdpServer(new IPEndPoint(IPAddress.Loopback, 0), false); - } + => new(new(IPAddress.Loopback, 0), false); private static IPEndPoint Peer(int port) - { - return new IPEndPoint(IPAddress.Loopback, port); - } + => new(IPAddress.Loopback, port); } diff --git a/tests/SquidStd.Tests/Persistence/DurableWriteTests.cs b/tests/SquidStd.Tests/Persistence/DurableWriteTests.cs index 4a554745..92ac939b 100644 --- a/tests/SquidStd.Tests/Persistence/DurableWriteTests.cs +++ b/tests/SquidStd.Tests/Persistence/DurableWriteTests.cs @@ -15,14 +15,16 @@ public async Task DurableJournal_AppendThenReadAll_RoundTrips() var path = Path.Combine(_dir, "world.journal.bin"); await using var journal = new BinaryJournalService(path, DurabilityMode.Durable); - await journal.AppendAsync(new JournalEntry - { - SequenceId = 1, - TimestampUnixMilliseconds = 1000, - TypeId = 1, - Operation = JournalEntityOperationType.Upsert, - Payload = [1, 2, 3] - }); + await journal.AppendAsync( + new() + { + SequenceId = 1, + TimestampUnixMilliseconds = 1000, + TypeId = 1, + Operation = JournalEntityOperationType.Upsert, + Payload = [1, 2, 3] + } + ); var entries = (await journal.ReadAllAsync()).ToArray(); Assert.Single(entries); @@ -35,7 +37,7 @@ public async Task DurableSnapshot_SaveThenLoad_RoundTrips() var service = new SnapshotService(_dir, ".snapshot.bin", DurabilityMode.Durable); var bucket = new EntitySnapshotBucket { TypeId = 1, TypeName = "Player", SchemaVersion = 1, Payload = [9, 9] }; - await service.SaveBucketAsync(bucket, lastSequenceId: 5); + await service.SaveBucketAsync(bucket, 5); var loaded = await service.LoadBucketAsync("Player", 1); Assert.NotNull(loaded); diff --git a/tests/SquidStd.Tests/Persistence/EntityStoreTests.cs b/tests/SquidStd.Tests/Persistence/EntityStoreTests.cs index 39f60a39..73fb5264 100644 --- a/tests/SquidStd.Tests/Persistence/EntityStoreTests.cs +++ b/tests/SquidStd.Tests/Persistence/EntityStoreTests.cs @@ -18,14 +18,14 @@ public EntityStoreTests() var serializer = new JsonDataSerializer(); IPersistenceEntityDescriptor descriptor = new PersistenceEntityDescriptor(serializer, serializer, 1, "Player", 1, p => p.Id); - _journal = new BinaryJournalService(Path.Combine(_dir, "world.journal.bin")); - _store = new EntityStore(_stateStore, _journal, descriptor); + _journal = new(Path.Combine(_dir, "world.journal.bin")); + _store = new(_stateStore, _journal, descriptor); } [Fact] public async Task Upsert_ThenGetById_ReturnsClone() { - await _store.UpsertAsync(new Player { Id = 1, Name = "Bob" }); + await _store.UpsertAsync(new() { Id = 1, Name = "Bob" }); var fetched = await _store.GetByIdAsync(1); @@ -36,7 +36,7 @@ public async Task Upsert_ThenGetById_ReturnsClone() [Fact] public async Task GetById_ReturnsDetachedClone() { - await _store.UpsertAsync(new Player { Id = 1, Tags = ["a"] }); + await _store.UpsertAsync(new() { Id = 1, Tags = ["a"] }); var first = await _store.GetByIdAsync(1); first!.Tags.Add("mutated"); @@ -48,7 +48,7 @@ public async Task GetById_ReturnsDetachedClone() [Fact] public async Task Upsert_AppendsToJournal() { - await _store.UpsertAsync(new Player { Id = 1 }); + await _store.UpsertAsync(new() { Id = 1 }); Assert.Single(await _journal.ReadAllAsync()); } @@ -56,7 +56,7 @@ public async Task Upsert_AppendsToJournal() [Fact] public async Task Remove_ExistingKey_ReturnsTrueAndJournals() { - await _store.UpsertAsync(new Player { Id = 1 }); + await _store.UpsertAsync(new() { Id = 1 }); Assert.True(await _store.RemoveAsync(1)); Assert.Null(await _store.GetByIdAsync(1)); @@ -73,8 +73,8 @@ public async Task Remove_MissingKey_ReturnsFalseAndDoesNotJournal() [Fact] public async Task CountAndGetAll_ReflectState() { - await _store.UpsertAsync(new Player { Id = 1 }); - await _store.UpsertAsync(new Player { Id = 2 }); + await _store.UpsertAsync(new() { Id = 1 }); + await _store.UpsertAsync(new() { Id = 2 }); Assert.Equal(2, await _store.CountAsync()); Assert.Equal(2, (await _store.GetAllAsync()).Count); @@ -83,8 +83,8 @@ public async Task CountAndGetAll_ReflectState() [Fact] public async Task Query_ReturnsQueryableClones() { - await _store.UpsertAsync(new Player { Id = 1, Name = "Alice" }); - await _store.UpsertAsync(new Player { Id = 2, Name = "Bob" }); + await _store.UpsertAsync(new() { Id = 1, Name = "Alice" }); + await _store.UpsertAsync(new() { Id = 2, Name = "Bob" }); var names = _store.Query().Where(p => p.Name.StartsWith('A')).Select(p => p.Name).ToArray(); diff --git a/tests/SquidStd.Tests/Persistence/EntityStoreWalOrderingTests.cs b/tests/SquidStd.Tests/Persistence/EntityStoreWalOrderingTests.cs index 4a600045..702c5ec1 100644 --- a/tests/SquidStd.Tests/Persistence/EntityStoreWalOrderingTests.cs +++ b/tests/SquidStd.Tests/Persistence/EntityStoreWalOrderingTests.cs @@ -18,7 +18,7 @@ public async Task Upsert_WhenJournalAppendThrows_LeavesInMemoryStateUnchanged() var stateStore = new PersistenceStateStore(); var store = new EntityStore(stateStore, new ThrowingJournalService(), descriptor); - await Assert.ThrowsAsync(async () => await store.UpsertAsync(new Player { Id = 1, Name = "Bob" })); + await Assert.ThrowsAsync(async () => await store.UpsertAsync(new() { Id = 1, Name = "Bob" })); // The failed durable append must NOT have applied the mutation in memory. Assert.Null(await store.GetByIdAsync(1)); @@ -34,7 +34,7 @@ public async Task Remove_WhenJournalAppendThrows_KeepsEntity() var stateStore = new PersistenceStateStore(); var ok = new CountingJournalService(); var store = new EntityStore(stateStore, ok, descriptor); - await store.UpsertAsync(new Player { Id = 1, Name = "Bob" }); + await store.UpsertAsync(new() { Id = 1, Name = "Bob" }); ok.FailNextAppend = true; await Assert.ThrowsAsync(async () => await store.RemoveAsync(1)); @@ -59,7 +59,8 @@ public ValueTask AppendBatchAsync(IReadOnlyList entries, Cancellat public ValueTask> ReadAllAsync(CancellationToken cancellationToken = default) => ValueTask.FromResult>([]); - public ValueTask ResetAsync(CancellationToken cancellationToken = default) => ValueTask.CompletedTask; + public ValueTask ResetAsync(CancellationToken cancellationToken = default) + => ValueTask.CompletedTask; public ValueTask TrimThroughSequenceAsync(long inclusiveSequenceId, CancellationToken cancellationToken = default) => ValueTask.CompletedTask; @@ -85,7 +86,8 @@ public ValueTask AppendBatchAsync(IReadOnlyList entries, Cancellat public ValueTask> ReadAllAsync(CancellationToken cancellationToken = default) => ValueTask.FromResult>([]); - public ValueTask ResetAsync(CancellationToken cancellationToken = default) => ValueTask.CompletedTask; + public ValueTask ResetAsync(CancellationToken cancellationToken = default) + => ValueTask.CompletedTask; public ValueTask TrimThroughSequenceAsync(long inclusiveSequenceId, CancellationToken cancellationToken = default) => ValueTask.CompletedTask; diff --git a/tests/SquidStd.Tests/Persistence/Integration/PersistenceEndToEndTests.cs b/tests/SquidStd.Tests/Persistence/Integration/PersistenceEndToEndTests.cs index 8f074528..b5fe218c 100644 --- a/tests/SquidStd.Tests/Persistence/Integration/PersistenceEndToEndTests.cs +++ b/tests/SquidStd.Tests/Persistence/Integration/PersistenceEndToEndTests.cs @@ -18,7 +18,7 @@ private PersistenceService Create() var journal = new BinaryJournalService(Path.Combine(_dir, config.JournalFileName)); var snapshot = new SnapshotService(_dir, config.SnapshotFileSuffix); - return new PersistenceService(registry, journal, snapshot, config, eventBus: null); + return new(registry, journal, snapshot, config); } [Fact] @@ -29,11 +29,11 @@ public async Task FullCycle_SnapshotRemoveTailCrashRestart_RestoresExactState() var service = Create(); await service.InitializeAsync(); var store = service.GetStore(); - await store.UpsertAsync(new Item { Id = 1, Label = "Sword", Quantity = 1 }); - await store.UpsertAsync(new Item { Id = 2, Label = "Potion", Quantity = 5 }); - await service.SaveSnapshotAsync(); // snapshot at seq 2 - await store.UpsertAsync(new Item { Id = 2, Label = "Potion", Quantity = 9 }); // tail update (seq 3) - await store.RemoveAsync(1); // tail remove (seq 4) + await store.UpsertAsync(new() { Id = 1, Label = "Sword", Quantity = 1 }); + await store.UpsertAsync(new() { Id = 2, Label = "Potion", Quantity = 5 }); + await service.SaveSnapshotAsync(); // snapshot at seq 2 + await store.UpsertAsync(new() { Id = 2, Label = "Potion", Quantity = 9 }); // tail update (seq 3) + await store.RemoveAsync(1); // tail remove (seq 4) var reloaded = Create(); await reloaded.InitializeAsync(); diff --git a/tests/SquidStd.Tests/Persistence/Internal/SnapshotEnvelopeCodecTests.cs b/tests/SquidStd.Tests/Persistence/Internal/SnapshotEnvelopeCodecTests.cs index 0b31d104..85ef2721 100644 --- a/tests/SquidStd.Tests/Persistence/Internal/SnapshotEnvelopeCodecTests.cs +++ b/tests/SquidStd.Tests/Persistence/Internal/SnapshotEnvelopeCodecTests.cs @@ -13,7 +13,7 @@ public void EncodeDecode_RoundTrips() Version = 1, LastSequenceId = 99, Checksum = 123456u, - Bucket = new EntitySnapshotBucket + Bucket = new() { TypeId = 3, TypeName = "PlayerData", diff --git a/tests/SquidStd.Tests/Persistence/PersistenceConfigTests.cs b/tests/SquidStd.Tests/Persistence/PersistenceConfigTests.cs index eef85fa2..e80df2f6 100644 --- a/tests/SquidStd.Tests/Persistence/PersistenceConfigTests.cs +++ b/tests/SquidStd.Tests/Persistence/PersistenceConfigTests.cs @@ -7,9 +7,7 @@ public class PersistenceConfigTests { [Fact] public void DurabilityMode_DefaultsToBuffered() - { - Assert.Equal(DurabilityMode.Buffered, new PersistenceConfig().DurabilityMode); - } + => Assert.Equal(DurabilityMode.Buffered, new PersistenceConfig().DurabilityMode); [Fact] public void DurabilityMode_CanBeSetToDurable() diff --git a/tests/SquidStd.Tests/Persistence/PersistenceEntityDescriptorTests.cs b/tests/SquidStd.Tests/Persistence/PersistenceEntityDescriptorTests.cs index 3fd58a16..cc16a102 100644 --- a/tests/SquidStd.Tests/Persistence/PersistenceEntityDescriptorTests.cs +++ b/tests/SquidStd.Tests/Persistence/PersistenceEntityDescriptorTests.cs @@ -9,12 +9,12 @@ private static PersistenceEntityDescriptor CreateDescriptor() { var serializer = new JsonDataSerializer(); - return new PersistenceEntityDescriptor(serializer, serializer, 1, "Player", 1, p => p.Id); + return new(serializer, serializer, 1, "Player", 1, p => p.Id); } [Fact] public void GetKey_UsesSelector() - => Assert.Equal(5, CreateDescriptor().GetKey(new Player { Id = 5 })); + => Assert.Equal(5, CreateDescriptor().GetKey(new() { Id = 5 })); [Fact] public void SerializeDeserializeEntity_RoundTrips() diff --git a/tests/SquidStd.Tests/Persistence/PersistenceEntityRegistryTests.cs b/tests/SquidStd.Tests/Persistence/PersistenceEntityRegistryTests.cs index 548011c7..84921539 100644 --- a/tests/SquidStd.Tests/Persistence/PersistenceEntityRegistryTests.cs +++ b/tests/SquidStd.Tests/Persistence/PersistenceEntityRegistryTests.cs @@ -10,7 +10,7 @@ private static PersistenceEntityDescriptor Descriptor(ushort typeId { var serializer = new JsonDataSerializer(); - return new PersistenceEntityDescriptor(serializer, serializer, typeId, "Player", 1, p => p.Id); + return new(serializer, serializer, typeId, "Player", 1, p => p.Id); } [Fact] diff --git a/tests/SquidStd.Tests/Persistence/PersistenceServiceTests.cs b/tests/SquidStd.Tests/Persistence/PersistenceServiceTests.cs index 63843288..39fce6fa 100644 --- a/tests/SquidStd.Tests/Persistence/PersistenceServiceTests.cs +++ b/tests/SquidStd.Tests/Persistence/PersistenceServiceTests.cs @@ -18,7 +18,7 @@ public sealed class PersistenceServiceTests : IDisposable var config = new PersistenceConfig { SaveDirectory = _dir, AutosaveInterval = TimeSpan.FromHours(1) }; var journal = new BinaryJournalService(Path.Combine(_dir, config.JournalFileName)); var snapshot = new SnapshotService(_dir, config.SnapshotFileSuffix); - var service = new PersistenceService(registry, journal, snapshot, config, eventBus: null); + var service = new PersistenceService(registry, journal, snapshot, config, null); return (service, registry); } @@ -29,7 +29,7 @@ public async Task SnapshotThenReload_RestoresState() var (service, _) = Create(); await service.InitializeAsync(); var store = service.GetStore(); - await store.UpsertAsync(new Player { Id = 1, Name = "Bob" }); + await store.UpsertAsync(new() { Id = 1, Name = "Bob" }); await service.SaveSnapshotAsync(); var (reloaded, _) = Create(); @@ -45,7 +45,7 @@ public async Task NoSnapshot_ReplaysJournal() { var (service, _) = Create(); await service.InitializeAsync(); - await service.GetStore().UpsertAsync(new Player { Id = 7, Name = "Eve" }); + await service.GetStore().UpsertAsync(new() { Id = 7, Name = "Eve" }); var (reloaded, _) = Create(); await reloaded.InitializeAsync(); // only journal exists, no snapshot @@ -60,9 +60,9 @@ public async Task SnapshotPlusJournalTail_ReplaysTail() var (service, _) = Create(); await service.InitializeAsync(); var store = service.GetStore(); - await store.UpsertAsync(new Player { Id = 1, Name = "First" }); + await store.UpsertAsync(new() { Id = 1, Name = "First" }); await service.SaveSnapshotAsync(); - await store.UpsertAsync(new Player { Id = 2, Name = "Second" }); + await store.UpsertAsync(new() { Id = 2, Name = "Second" }); var (reloaded, _) = Create(); await reloaded.InitializeAsync(); @@ -76,7 +76,7 @@ public async Task SaveSnapshot_TrimsJournal() { var (service, _) = Create(); await service.InitializeAsync(); - await service.GetStore().UpsertAsync(new Player { Id = 1 }); + await service.GetStore().UpsertAsync(new() { Id = 1 }); await service.SaveSnapshotAsync(); var journal = new BinaryJournalService(Path.Combine(_dir, "world.journal.bin")); @@ -95,11 +95,11 @@ public async Task PartialSnapshotSave_ReplaysLaggingTypeFromJournal() // (untrimmed) journal — the exact state a partial snapshot save leaves behind. var journal = new BinaryJournalService(Path.Combine(_dir, config.JournalFileName)); var faulty = new FailOnSecondSaveSnapshotService(new SnapshotService(_dir, config.SnapshotFileSuffix)); - var service = new PersistenceService(BuildRegistry(serializer), journal, faulty, config, eventBus: null); + var service = new PersistenceService(BuildRegistry(serializer), journal, faulty, config); await service.InitializeAsync(); - await service.GetStore().UpsertAsync(new Player { Id = 1, Name = "First" }); - await service.GetStore().UpsertAsync(new Item { Id = 1, Name = "Sword" }); + await service.GetStore().UpsertAsync(new() { Id = 1, Name = "First" }); + await service.GetStore().UpsertAsync(new() { Id = 1, Name = "Sword" }); await Assert.ThrowsAnyAsync(async () => await service.SaveSnapshotAsync()); await journal.DisposeAsync(); @@ -108,7 +108,10 @@ public async Task PartialSnapshotSave_ReplaysLaggingTypeFromJournal() // watermark would skip the journal entry for whichever type's snapshot did persist, losing it. var reloadJournal = new BinaryJournalService(Path.Combine(_dir, config.JournalFileName)); var reloaded = new PersistenceService( - BuildRegistry(serializer), reloadJournal, new SnapshotService(_dir, config.SnapshotFileSuffix), config, eventBus: null + BuildRegistry(serializer), + reloadJournal, + new SnapshotService(_dir, config.SnapshotFileSuffix), + config ); await reloaded.InitializeAsync(); @@ -150,8 +153,10 @@ private sealed class Item public string Name { get; set; } = string.Empty; } - /// Delegates to a real snapshot service but throws on the second bucket save, simulating a - /// snapshot run that persists one type's bucket and then fails before the next. + /// + /// Delegates to a real snapshot service but throws on the second bucket save, simulating a + /// snapshot run that persists one type's bucket and then fails before the next. + /// private sealed class FailOnSecondSaveSnapshotService : ISnapshotService { private readonly ISnapshotService _inner; @@ -163,7 +168,9 @@ public FailOnSecondSaveSnapshotService(ISnapshotService inner) } public ValueTask SaveBucketAsync( - EntitySnapshotBucket bucket, long lastSequenceId, CancellationToken cancellationToken = default + EntitySnapshotBucket bucket, + long lastSequenceId, + CancellationToken cancellationToken = default ) { if (++_saveCount >= 2) @@ -175,15 +182,13 @@ public ValueTask SaveBucketAsync( } public ValueTask LoadBucketAsync( - string typeName, ushort typeId, CancellationToken cancellationToken = default + string typeName, + ushort typeId, + CancellationToken cancellationToken = default ) - { - return _inner.LoadBucketAsync(typeName, typeId, cancellationToken); - } + => _inner.LoadBucketAsync(typeName, typeId, cancellationToken); public ValueTask DeleteBucketAsync(string typeName, ushort typeId, CancellationToken cancellationToken = default) - { - return _inner.DeleteBucketAsync(typeName, typeId, cancellationToken); - } + => _inner.DeleteBucketAsync(typeName, typeId, cancellationToken); } } diff --git a/tests/SquidStd.Tests/Persistence/PersistenceSnapshotConcurrencyTests.cs b/tests/SquidStd.Tests/Persistence/PersistenceSnapshotConcurrencyTests.cs index aba0e03b..b9b0734d 100644 --- a/tests/SquidStd.Tests/Persistence/PersistenceSnapshotConcurrencyTests.cs +++ b/tests/SquidStd.Tests/Persistence/PersistenceSnapshotConcurrencyTests.cs @@ -8,7 +8,8 @@ namespace SquidStd.Tests.Persistence; public sealed class PersistenceSnapshotConcurrencyTests : IDisposable { - private readonly string _dir = Path.Combine(Path.GetTempPath(), "squidstd-snapshot-conc-" + Guid.NewGuid().ToString("N")); + private readonly string _dir = + Path.Combine(Path.GetTempPath(), "squidstd-snapshot-conc-" + Guid.NewGuid().ToString("N")); [Fact] public async Task SaveSnapshotAsync_ConcurrentCalls_DoNotInterleave() @@ -19,14 +20,12 @@ public async Task SaveSnapshotAsync_ConcurrentCalls_DoNotInterleave() var config = new PersistenceConfig { SaveDirectory = _dir, AutosaveInterval = TimeSpan.FromHours(1) }; var journal = new BinaryJournalService(Path.Combine(_dir, config.JournalFileName)); var snapshot = new ConcurrencyProbeSnapshotService(); - var service = new PersistenceService(registry, journal, snapshot, config, eventBus: null); + var service = new PersistenceService(registry, journal, snapshot, config, null); await service.InitializeAsync(); - await service.GetStore().UpsertAsync(new Player { Id = 1, Name = "Bob" }); + await service.GetStore().UpsertAsync(new() { Id = 1, Name = "Bob" }); - await Task.WhenAll( - Enumerable.Range(0, 8).Select(async _ => await service.SaveSnapshotAsync()) - ); + await Task.WhenAll(Enumerable.Range(0, 8).Select(async _ => await service.SaveSnapshotAsync())); await journal.DisposeAsync(); Assert.Equal(1, snapshot.MaxObservedConcurrency); // never two snapshot operations at once @@ -53,7 +52,9 @@ private sealed class ConcurrencyProbeSnapshotService : ISnapshotService public int MaxObservedConcurrency { get; private set; } public async ValueTask SaveBucketAsync( - EntitySnapshotBucket bucket, long lastSequenceId, CancellationToken cancellationToken = default + EntitySnapshotBucket bucket, + long lastSequenceId, + CancellationToken cancellationToken = default ) { var now = Interlocked.Increment(ref _active); @@ -62,7 +63,11 @@ public async ValueTask SaveBucketAsync( Interlocked.Decrement(ref _active); } - public ValueTask LoadBucketAsync(string typeName, ushort typeId, CancellationToken cancellationToken = default) + public ValueTask LoadBucketAsync( + string typeName, + ushort typeId, + CancellationToken cancellationToken = default + ) => ValueTask.FromResult(null); public ValueTask DeleteBucketAsync(string typeName, ushort typeId, CancellationToken cancellationToken = default) diff --git a/tests/SquidStd.Tests/Persistence/SnapshotChecksumScopeTests.cs b/tests/SquidStd.Tests/Persistence/SnapshotChecksumScopeTests.cs index 6116259f..b996f958 100644 --- a/tests/SquidStd.Tests/Persistence/SnapshotChecksumScopeTests.cs +++ b/tests/SquidStd.Tests/Persistence/SnapshotChecksumScopeTests.cs @@ -16,7 +16,7 @@ private static EntitySnapshotBucket Bucket() public async Task LoadBucketAsync_WhenLastSequenceIdCorrupted_RejectsSnapshot() { var service = new SnapshotService(_dir, ".snapshot.bin"); - await service.SaveBucketAsync(Bucket(), lastSequenceId: 42); + await service.SaveBucketAsync(Bucket(), 42); var file = Directory.GetFiles(_dir, "*.snapshot.bin").Single(); var bytes = await File.ReadAllBytesAsync(file); @@ -30,17 +30,19 @@ public async Task LoadBucketAsync_WhenLastSequenceIdCorrupted_RejectsSnapshot() public async Task LoadBucketAsync_LegacyVersion1File_StillLoads() { var service = new SnapshotService(_dir, ".snapshot.bin"); - await service.SaveBucketAsync(Bucket(), lastSequenceId: 7); // creates the file at the correct path + await service.SaveBucketAsync(Bucket(), 7); // creates the file at the correct path // Overwrite it with a legacy Version-1 envelope (payload-only checksum), as written before this change. var file = Directory.GetFiles(_dir, "*.snapshot.bin").Single(); - var legacy = SnapshotEnvelopeCodec.Encode(new SnapshotFileEnvelope - { - Version = 1, - LastSequenceId = 7, - Checksum = ChecksumUtils.Compute(Bucket().Payload), - Bucket = Bucket() - }); + var legacy = SnapshotEnvelopeCodec.Encode( + new() + { + Version = 1, + LastSequenceId = 7, + Checksum = ChecksumUtils.Compute(Bucket().Payload), + Bucket = Bucket() + } + ); await File.WriteAllBytesAsync(file, legacy); var loaded = await service.LoadBucketAsync("Player", 1); diff --git a/tests/SquidStd.Tests/Persistence/SnapshotFilenameTests.cs b/tests/SquidStd.Tests/Persistence/SnapshotFilenameTests.cs index 065db64e..95dfaa39 100644 --- a/tests/SquidStd.Tests/Persistence/SnapshotFilenameTests.cs +++ b/tests/SquidStd.Tests/Persistence/SnapshotFilenameTests.cs @@ -16,8 +16,8 @@ public async Task SameTypeName_DifferentTypeId_ProduceDistinctFiles() { var service = new SnapshotService(_dir, ".snapshot.bin"); - await service.SaveBucketAsync(Bucket(1, 0xAA), lastSequenceId: 1); - await service.SaveBucketAsync(Bucket(2, 0xBB), lastSequenceId: 2); + await service.SaveBucketAsync(Bucket(1, 0xAA), 1); + await service.SaveBucketAsync(Bucket(2, 0xBB), 2); Assert.Equal(2, Directory.GetFiles(_dir, "*.snapshot.bin").Length); @@ -34,7 +34,7 @@ public async Task LoadBucketAsync_MigratesLegacyNamedFile() var service = new SnapshotService(_dir, ".snapshot.bin"); // Write a file at the OLD path (no TypeId) by saving then renaming to the legacy name. - await service.SaveBucketAsync(Bucket(1, 0x7), lastSequenceId: 9); + await service.SaveBucketAsync(Bucket(1, 0x7), 9); var newPath = Directory.GetFiles(_dir, "*.snapshot.bin").Single(); var legacyPath = Path.Combine(_dir, StringUtils.ToSnakeCase("Player") + ".snapshot.bin"); File.Move(newPath, legacyPath); @@ -43,7 +43,7 @@ public async Task LoadBucketAsync_MigratesLegacyNamedFile() Assert.NotNull(loaded); Assert.Equal(9, loaded.LastSequenceId); - Assert.False(File.Exists(legacyPath)); // legacy file was migrated away + Assert.False(File.Exists(legacyPath)); // legacy file was migrated away Assert.True(File.Exists(Path.Combine(_dir, "player_1.snapshot.bin"))); // to the new TypeId path } diff --git a/tests/SquidStd.Tests/PluginAbstractions/PluginContextTests.cs b/tests/SquidStd.Tests/PluginAbstractions/PluginContextTests.cs index c44c0461..6b49f609 100644 --- a/tests/SquidStd.Tests/PluginAbstractions/PluginContextTests.cs +++ b/tests/SquidStd.Tests/PluginAbstractions/PluginContextTests.cs @@ -6,15 +6,11 @@ public class PluginContextTests { [Fact] public void Data_IsEmptyByDefault() - { - Assert.Empty(new PluginContext().Data); - } + => Assert.Empty(new PluginContext().Data); [Fact] public void GetData_MissingKey_Throws() - { - Assert.Throws(() => { _ = new PluginContext().GetData("missing"); }); - } + => Assert.Throws(() => { _ = new PluginContext().GetData("missing"); }); [Fact] public void GetData_ReferenceType_ReturnsStoredValue() diff --git a/tests/SquidStd.Tests/PluginAbstractions/PluginMetadataTests.cs b/tests/SquidStd.Tests/PluginAbstractions/PluginMetadataTests.cs index 5d27d2af..bbaa345e 100644 --- a/tests/SquidStd.Tests/PluginAbstractions/PluginMetadataTests.cs +++ b/tests/SquidStd.Tests/PluginAbstractions/PluginMetadataTests.cs @@ -11,13 +11,13 @@ public void Constructor_SetsRequiredProperties() { Id = "squidstd.weather", Name = "Weather", - Version = new Version(2, 1, 0), + Version = new(2, 1, 0), Author = "squid" }; Assert.Equal("squidstd.weather", metadata.Id); Assert.Equal("Weather", metadata.Name); - Assert.Equal(new Version(2, 1, 0), metadata.Version); + Assert.Equal(new(2, 1, 0), metadata.Version); Assert.Equal("squid", metadata.Author); } @@ -28,7 +28,7 @@ public void Dependencies_CanBeProvided() { Id = "id", Name = "name", - Version = new Version(1, 0), + Version = new(1, 0), Author = "author", Dependencies = ["squidstd.core", "squidstd.net"] }; @@ -43,7 +43,7 @@ public void Dependencies_DefaultsToEmpty() { Id = "id", Name = "name", - Version = new Version(1, 0), + Version = new(1, 0), Author = "author" }; @@ -57,7 +57,7 @@ public void Description_DefaultsToNull() { Id = "id", Name = "name", - Version = new Version(1, 0), + Version = new(1, 0), Author = "author" }; diff --git a/tests/SquidStd.Tests/PluginAbstractions/SquidStdPluginTests.cs b/tests/SquidStd.Tests/PluginAbstractions/SquidStdPluginTests.cs index ea081e1d..2fc16ed9 100644 --- a/tests/SquidStd.Tests/PluginAbstractions/SquidStdPluginTests.cs +++ b/tests/SquidStd.Tests/PluginAbstractions/SquidStdPluginTests.cs @@ -25,7 +25,7 @@ public void Configure_RegistersServicesIntoContainer() using var container = new Container(); var plugin = new FakePlugin(); - ((ISquidStdPlugin)plugin).Configure(container, new PluginContext()); + ((ISquidStdPlugin)plugin).Configure(container, new()); Assert.Same(plugin, container.Resolve()); } @@ -36,6 +36,6 @@ public void Metadata_ExposesPluginIdentity() ISquidStdPlugin plugin = new FakePlugin(); Assert.Equal("squidstd.fake", plugin.Metadata.Id); - Assert.Equal(new Version(1, 2, 3), plugin.Metadata.Version); + Assert.Equal(new(1, 2, 3), plugin.Metadata.Version); } } diff --git a/tests/SquidStd.Tests/Scripting/Lua/LuaEventBridgeTests.cs b/tests/SquidStd.Tests/Scripting/Lua/LuaEventBridgeTests.cs index 212b0e28..000afc41 100644 --- a/tests/SquidStd.Tests/Scripting/Lua/LuaEventBridgeTests.cs +++ b/tests/SquidStd.Tests/Scripting/Lua/LuaEventBridgeTests.cs @@ -12,13 +12,13 @@ public void Invoke_ConvertsNestedPayloadToLuaTable() var bridge = new LuaEventBridge(); bridge.Attach(script); var callback = script.DoString( - """ - return function(payload) - return payload.actor.name .. ':' .. payload.values[2] - end - """ - ) - .Function; + """ + return function(payload) + return payload.actor.name .. ':' .. payload.values[2] + end + """ + ) + .Function; var result = bridge.Invoke( callback, @@ -49,16 +49,16 @@ public void Publish_InvokesRegisteredCallbacksCaseInsensitively() var bridge = new LuaEventBridge(); bridge.Attach(script); var callback = script.DoString( - """ - calls = 0 - captured = nil - return function(payload) - calls = calls + 1 - captured = payload.name - end - """ - ) - .Function; + """ + calls = 0 + captured = nil + return function(payload) + calls = calls + 1 + captured = payload.name + end + """ + ) + .Function; bridge.Register("Spawned", callback); bridge.Publish("spawned", new Dictionary { ["name"] = "slime" }); diff --git a/tests/SquidStd.Tests/Scripting/Lua/LuaModuleTests.cs b/tests/SquidStd.Tests/Scripting/Lua/LuaModuleTests.cs index 450ea071..f22dfd03 100644 --- a/tests/SquidStd.Tests/Scripting/Lua/LuaModuleTests.cs +++ b/tests/SquidStd.Tests/Scripting/Lua/LuaModuleTests.cs @@ -44,7 +44,7 @@ public void RandomModule_PickRejectsEmptyTable() { var module = new RandomModule(); - Assert.Throws(() => module.Pick(new Table(new Script()))); + Assert.Throws(() => module.Pick(new(new()))); } [Fact] @@ -52,14 +52,14 @@ public void RandomModule_WeightedRejectsEntriesWithoutPositiveWeight() { var script = new Script(); var entries = script.DoString( - """ - return { - { value = 'a', weight = 0 }, - { value = 'b', weight = -3 } - } - """ - ) - .Table; + """ + return { + { value = 'a', weight = 0 }, + { value = 'b', weight = -3 } + } + """ + ) + .Table; var module = new RandomModule(); Assert.Throws(() => module.Weighted(entries)); @@ -71,18 +71,12 @@ private sealed class CapturingLuaEventBridge : ILuaEventBridge public string? EventName { get; private set; } - public void Attach(Script script) - { - } + public void Attach(Script script) { } public DynValue Invoke(Closure callback, IReadOnlyDictionary payload) - { - return DynValue.Nil; - } + => DynValue.Nil; - public void Publish(string eventName, IReadOnlyDictionary payload) - { - } + public void Publish(string eventName, IReadOnlyDictionary payload) { } public void Register(string eventName, Closure callback) { diff --git a/tests/SquidStd.Tests/Scripting/Lua/LuaScriptEngineServiceTests.cs b/tests/SquidStd.Tests/Scripting/Lua/LuaScriptEngineServiceTests.cs index 057b592c..3cb6f157 100644 --- a/tests/SquidStd.Tests/Scripting/Lua/LuaScriptEngineServiceTests.cs +++ b/tests/SquidStd.Tests/Scripting/Lua/LuaScriptEngineServiceTests.cs @@ -1,7 +1,5 @@ using DryIoc; using MoonSharp.Interpreter; -using SquidStd.Core.Directories; -using SquidStd.Scripting.Lua.Data.Config; using SquidStd.Scripting.Lua.Data.Internal; using SquidStd.Scripting.Lua.Data.Scripts; using SquidStd.Scripting.Lua.Interfaces.Events; @@ -130,7 +128,7 @@ public void RegisteredUserData_CanBeConstructedFromLuaWithMoreThanFourArguments( container, loadedUserData: [ - new ScriptUserData { UserType = typeof(FiveArgumentUserData) } + new() { UserType = typeof(FiveArgumentUserData) } ] ); @@ -191,10 +189,10 @@ private static LuaScriptEngineService CreateEngine( var luarcDirectory = temp.Combine("luarc"); Directory.CreateDirectory(scriptsDirectory); - return new LuaScriptEngineService( - new DirectoriesConfig(temp.Path, []), + return new( + new(temp.Path, []), container, - new LuaEngineConfig(luarcDirectory, scriptsDirectory, "SquidStd", "1.0.0"), + new(luarcDirectory, scriptsDirectory, "SquidStd", "1.0.0"), scriptModules, loadedUserData ); @@ -210,25 +208,17 @@ private sealed class CapturingLuaEventBridge : ILuaEventBridge public Script? AttachedScript { get; private set; } public void Attach(Script script) - { - AttachedScript = script; - } + => AttachedScript = script; public DynValue Invoke( Closure callback, IReadOnlyDictionary payload ) - { - return DynValue.Nil; - } + => DynValue.Nil; - public void Publish(string eventName, IReadOnlyDictionary payload) - { - } + public void Publish(string eventName, IReadOnlyDictionary payload) { } - public void Register(string eventName, Closure callback) - { - } + public void Register(string eventName, Closure callback) { } } private sealed record LimitConfig(string Name, int Count); diff --git a/tests/SquidStd.Tests/Scripting/Lua/LuaScriptLoaderTests.cs b/tests/SquidStd.Tests/Scripting/Lua/LuaScriptLoaderTests.cs index 4ed4ae4f..df24478d 100644 --- a/tests/SquidStd.Tests/Scripting/Lua/LuaScriptLoaderTests.cs +++ b/tests/SquidStd.Tests/Scripting/Lua/LuaScriptLoaderTests.cs @@ -1,4 +1,3 @@ -using MoonSharp.Interpreter; using SquidStd.Scripting.Lua.Loaders; using SquidStd.Tests.Support; @@ -21,9 +20,7 @@ public void AddSearchDirectory_AddsAdditionalLookupRoot() [Fact] public void Constructor_EmptySearchDirectoriesThrows() - { - Assert.Throws(() => new LuaScriptLoader(Array.Empty())); - } + => Assert.Throws(() => new LuaScriptLoader(Array.Empty())); [Fact] public void LoadFile_LoadsContentFromFirstMatchingSearchDirectory() @@ -33,7 +30,7 @@ public void LoadFile_LoadsContentFromFirstMatchingSearchDirectory() File.WriteAllText(second.Combine("feature.lua"), "return 'loaded'"); var loader = new LuaScriptLoader([first.Path, second.Path]); - var content = loader.LoadFile("feature.lua", new Table(new Script())); + var content = loader.LoadFile("feature.lua", new(new())); Assert.Equal("return 'loaded'", content); } diff --git a/tests/SquidStd.Tests/Scripting/Lua/ScriptResultBuilderTests.cs b/tests/SquidStd.Tests/Scripting/Lua/ScriptResultBuilderTests.cs index be23b5ba..2aeb3623 100644 --- a/tests/SquidStd.Tests/Scripting/Lua/ScriptResultBuilderTests.cs +++ b/tests/SquidStd.Tests/Scripting/Lua/ScriptResultBuilderTests.cs @@ -8,8 +8,8 @@ public class ScriptResultBuilderTests public void CreateError_BuildsFailedResult() { var result = ScriptResultBuilder.CreateError() - .WithMessage("failed") - .Build(); + .WithMessage("failed") + .Build(); Assert.False(result.Success); Assert.Equal("failed", result.Message); @@ -20,9 +20,9 @@ public void CreateError_BuildsFailedResult() public void CreateSuccess_BuildsSuccessfulResult() { var result = ScriptResultBuilder.CreateSuccess() - .WithMessage("ok") - .WithData(42) - .Build(); + .WithMessage("ok") + .WithData(42) + .Build(); Assert.True(result.Success); Assert.Equal("ok", result.Message); diff --git a/tests/SquidStd.Tests/Scripting/Lua/TableExtensionsTests.cs b/tests/SquidStd.Tests/Scripting/Lua/TableExtensionsTests.cs index 0ffdd12b..f0516e64 100644 --- a/tests/SquidStd.Tests/Scripting/Lua/TableExtensionsTests.cs +++ b/tests/SquidStd.Tests/Scripting/Lua/TableExtensionsTests.cs @@ -10,15 +10,15 @@ public void ToProxy_DelegatesInterfaceCallsToLuaFunctions() { var script = new Script(); var table = script.DoString( - """ - return { - Sum = function(left, right) - return left + right - end - } - """ - ) - .Table; + """ + return { + Sum = function(left, right) + return left + right + end + } + """ + ) + .Table; var proxy = table.ToProxy(); @@ -28,7 +28,7 @@ public void ToProxy_DelegatesInterfaceCallsToLuaFunctions() [Fact] public void ToProxy_MissingFunctionThrowsMissingMethodException() { - var proxy = new Table(new Script()).ToProxy(); + var proxy = new Table(new()).ToProxy(); Assert.Throws(() => proxy.Sum(1, 2)); } diff --git a/tests/SquidStd.Tests/Search/ElasticExpressionTranslatorTests.cs b/tests/SquidStd.Tests/Search/ElasticExpressionTranslatorTests.cs index e77e8f41..848f3497 100644 --- a/tests/SquidStd.Tests/Search/ElasticExpressionTranslatorTests.cs +++ b/tests/SquidStd.Tests/Search/ElasticExpressionTranslatorTests.cs @@ -18,9 +18,7 @@ public void Match_ProducesMatchClause() [Fact] public void UnsupportedExpression_Throws() - { - Assert.Throws(() => Translate(s => s.Where(d => d.Name.ToUpperInvariant() == "X"))); - } + => Assert.Throws(() => Translate(s => s.Where(d => d.Name.ToUpperInvariant() == "X"))); [Fact] public void Where_Equality_ProducesTermOnKeyword() @@ -68,34 +66,22 @@ private TranslateOnlyQueryable(Expression expression) } public IEnumerator GetEnumerator() - { - throw new NotSupportedException(); - } + => throw new NotSupportedException(); IEnumerator IEnumerable.GetEnumerator() - { - throw new NotSupportedException(); - } + => throw new NotSupportedException(); public IQueryable CreateQuery(Expression expression) - { - return new TranslateOnlyQueryable(expression); - } + => new TranslateOnlyQueryable(expression); public IQueryable CreateQuery(Expression expression) - { - return new TranslateOnlyQueryable(expression); - } + => new TranslateOnlyQueryable(expression); public object? Execute(Expression expression) - { - throw new NotSupportedException(); - } + => throw new NotSupportedException(); public TResult Execute(Expression expression) - { - throw new NotSupportedException(); - } + => throw new NotSupportedException(); } private sealed record Doc(string Status, int Total, string Name) : IIndexableEntity diff --git a/tests/SquidStd.Tests/Search/SearchIndexNameResolverTests.cs b/tests/SquidStd.Tests/Search/SearchIndexNameResolverTests.cs index 26072076..c265afea 100644 --- a/tests/SquidStd.Tests/Search/SearchIndexNameResolverTests.cs +++ b/tests/SquidStd.Tests/Search/SearchIndexNameResolverTests.cs @@ -23,9 +23,7 @@ public void Resolve_ExpandsEnvVariable() [Fact] public void Resolve_FallsBackToLowercasedTypeName() - { - Assert.Equal("plaindoc", SearchIndexNameResolver.Resolve(typeof(PlainDoc))); - } + => Assert.Equal("plaindoc", SearchIndexNameResolver.Resolve(typeof(PlainDoc))); [Fact] public void Resolve_Throws_WhenRequiredVariableMissing() @@ -36,9 +34,7 @@ public void Resolve_Throws_WhenRequiredVariableMissing() [Fact] public void Resolve_UsesAttribute_Lowercased() - { - Assert.Equal("orders", SearchIndexNameResolver.Resolve(typeof(AttributedDoc))); - } + => Assert.Equal("orders", SearchIndexNameResolver.Resolve(typeof(AttributedDoc))); [Fact] public void Resolve_UsesDefault_WhenVariableMissing() diff --git a/tests/SquidStd.Tests/Secrets/Aws/AwsSecretsManagerStoreTests.cs b/tests/SquidStd.Tests/Secrets/Aws/AwsSecretsManagerStoreTests.cs index e3a148cd..b42a6b02 100644 --- a/tests/SquidStd.Tests/Secrets/Aws/AwsSecretsManagerStoreTests.cs +++ b/tests/SquidStd.Tests/Secrets/Aws/AwsSecretsManagerStoreTests.cs @@ -1,4 +1,3 @@ -using SquidStd.Secrets.Aws.Data; using SquidStd.Secrets.Aws.Services; using SquidStd.Tests.Secrets.Aws.Support; @@ -18,7 +17,7 @@ public AwsSecretsManagerStoreTests(LocalStackSecretsFixture localStack) public async Task Set_Get_Exists_List_Delete_RoundTrips() { var prefix = "test-" + Guid.NewGuid().ToString("N") + "/"; - var store = new AwsSecretsManagerStore(new AwsSecretsManagerOptions { Aws = _localStack.Aws, NamePrefix = prefix }); + var store = new AwsSecretsManagerStore(new() { Aws = _localStack.Aws, NamePrefix = prefix }); Assert.Null(await store.GetAsync("db/main")); Assert.False(await store.ExistsAsync("db/main")); @@ -30,6 +29,7 @@ public async Task Set_Get_Exists_List_Delete_RoundTrips() Assert.True(await store.ExistsAsync("db/main")); var names = new List(); + await foreach (var name in store.ListNamesAsync()) { names.Add(name); diff --git a/tests/SquidStd.Tests/Secrets/Aws/KmsSecretProtectorTests.cs b/tests/SquidStd.Tests/Secrets/Aws/KmsSecretProtectorTests.cs index 86319911..f0ac1ab1 100644 --- a/tests/SquidStd.Tests/Secrets/Aws/KmsSecretProtectorTests.cs +++ b/tests/SquidStd.Tests/Secrets/Aws/KmsSecretProtectorTests.cs @@ -1,7 +1,6 @@ using System.Text; using Amazon.KeyManagementService; -using Amazon.KeyManagementService.Model; -using SquidStd.Secrets.Aws.Data; +using Amazon.Runtime; using SquidStd.Secrets.Aws.Services; using SquidStd.Tests.Secrets.Aws.Support; @@ -21,7 +20,7 @@ public KmsSecretProtectorTests(LocalStackSecretsFixture localStack) public async Task Protect_Unprotect_RoundTripsLargePayload_WithoutPlaintext() { var keyId = await CreateKeyAsync(); - var protector = new KmsSecretProtector(new KmsSecretProtectorOptions { Aws = _localStack.Aws, KeyId = keyId }); + var protector = new KmsSecretProtector(new() { Aws = _localStack.Aws, KeyId = keyId }); var plaintext = Encoding.UTF8.GetBytes(new string('s', 10_000)); // > 4 KB → exercises envelope var blob = protector.Protect(plaintext); @@ -34,11 +33,11 @@ public async Task Protect_Unprotect_RoundTripsLargePayload_WithoutPlaintext() private async Task CreateKeyAsync() { using var kms = new AmazonKeyManagementServiceClient( - new Amazon.Runtime.BasicAWSCredentials("test", "test"), + new BasicAWSCredentials("test", "test"), new AmazonKeyManagementServiceConfig { ServiceURL = _localStack.Aws.ServiceUrl, AuthenticationRegion = "us-east-1" } ); - var created = await kms.CreateKeyAsync(new CreateKeyRequest()); + var created = await kms.CreateKeyAsync(new()); return created.KeyMetadata.KeyId; } diff --git a/tests/SquidStd.Tests/Secrets/Aws/Support/LocalStackSecretsFixture.cs b/tests/SquidStd.Tests/Secrets/Aws/Support/LocalStackSecretsFixture.cs index ed07dbe2..6720d79f 100644 --- a/tests/SquidStd.Tests/Secrets/Aws/Support/LocalStackSecretsFixture.cs +++ b/tests/SquidStd.Tests/Secrets/Aws/Support/LocalStackSecretsFixture.cs @@ -9,23 +9,20 @@ public sealed class LocalStackSecretsFixture : IAsyncLifetime private readonly LocalStackContainer _container = new LocalStackBuilder().WithImage("localstack/localstack:3").Build(); - public AwsConfigEntry Aws => new() - { - Region = "us-east-1", - AccessKey = "test", - SecretKey = "test", - ServiceUrl = _container.GetConnectionString() - }; + public AwsConfigEntry Aws + => new() + { + Region = "us-east-1", + AccessKey = "test", + SecretKey = "test", + ServiceUrl = _container.GetConnectionString() + }; public Task DisposeAsync() - { - return _container.DisposeAsync().AsTask(); - } + => _container.DisposeAsync().AsTask(); public Task InitializeAsync() - { - return _container.StartAsync(); - } + => _container.StartAsync(); } [CollectionDefinition(Name)] diff --git a/tests/SquidStd.Tests/Security/FileSecretStoreEnumerationTests.cs b/tests/SquidStd.Tests/Security/FileSecretStoreEnumerationTests.cs index d4887644..04d5fbeb 100644 --- a/tests/SquidStd.Tests/Security/FileSecretStoreEnumerationTests.cs +++ b/tests/SquidStd.Tests/Security/FileSecretStoreEnumerationTests.cs @@ -25,12 +25,14 @@ public async Task ListNamesAsync_ReturnsSetNames_AndHonoursPrefix() await store.SetAsync("api/key", "c"); var all = new List(); + await foreach (var name in store.ListNamesAsync()) { all.Add(name); } var db = new List(); + await foreach (var name in store.ListNamesAsync("db/")) { db.Add(name); diff --git a/tests/SquidStd.Tests/Security/SecretsTests.cs b/tests/SquidStd.Tests/Security/SecretsTests.cs index ba6ed233..1c62ecda 100644 --- a/tests/SquidStd.Tests/Security/SecretsTests.cs +++ b/tests/SquidStd.Tests/Security/SecretsTests.cs @@ -22,7 +22,7 @@ public void AesGcmSecretProtector_Protect_Unprotect_RoundTripsWithoutPlaintext() try { Environment.SetEnvironmentVariable(variableName, Convert.ToBase64String(key)); - var protector = new AesGcmSecretProtector(new SecretsConfig { KeyEnvironmentVariable = variableName }); + var protector = new AesGcmSecretProtector(new() { KeyEnvironmentVariable = variableName }); var plaintext = Encoding.UTF8.GetBytes("super-secret-value"); var protectedData = protector.Protect(plaintext); @@ -49,7 +49,7 @@ public void AesGcmSecretProtector_WhenKeyEnvironmentVariableIsMissing_UsesDefaul { Environment.SetEnvironmentVariable(variableName, null); Log.Logger = new LoggerConfiguration().MinimumLevel.Verbose().WriteTo.Sink(sink).CreateLogger(); - var protector = new AesGcmSecretProtector(new SecretsConfig { KeyEnvironmentVariable = variableName }); + var protector = new AesGcmSecretProtector(new() { KeyEnvironmentVariable = variableName }); var plaintext = Encoding.UTF8.GetBytes("default-key-secret"); var protectedData = protector.Protect(plaintext); @@ -110,8 +110,6 @@ private sealed class CapturingSink : ILogEventSink public IReadOnlyList Events => _events; public void Emit(LogEvent logEvent) - { - _events.Add(logEvent); - } + => _events.Add(logEvent); } } diff --git a/tests/SquidStd.Tests/Services/Core/CommandDispatcherTests.cs b/tests/SquidStd.Tests/Services/Core/CommandDispatcherTests.cs index f1d02ec7..9208cba0 100644 --- a/tests/SquidStd.Tests/Services/Core/CommandDispatcherTests.cs +++ b/tests/SquidStd.Tests/Services/Core/CommandDispatcherTests.cs @@ -30,7 +30,7 @@ public async Task DispatchAsync_WhenNoHandler_ReturnsUnmatched() { using var dispatcher = new CommandDispatcher(); - var result = await dispatcher.DispatchAsync(new PingCommand("x"), new Session()); + var result = await dispatcher.DispatchAsync(new PingCommand("x"), new()); Assert.False(result.Matched); Assert.Equal(0, result.HandlerCount); @@ -45,7 +45,7 @@ public async Task DispatchAsync_IsolatesFaults() dispatcher.RegisterHandler(new ThrowingHandler()); dispatcher.RegisterHandler(healthy); - var result = await dispatcher.DispatchAsync(new PingCommand("go"), new Session()); + var result = await dispatcher.DispatchAsync(new PingCommand("go"), new()); Assert.True(result.Matched); Assert.Equal(2, result.HandlerCount); @@ -61,7 +61,8 @@ public async Task DispatchAsync_WhenCancelled_Propagates() using var dispatcher = new CommandDispatcher(); using var cts = new CancellationTokenSource(); await cts.CancelAsync(); - dispatcher.Subscribe((_, _, token) => + dispatcher.Subscribe( + (_, _, token) => { token.ThrowIfCancellationRequested(); @@ -69,9 +70,10 @@ public async Task DispatchAsync_WhenCancelled_Propagates() } ); - await Assert.ThrowsAsync(() => dispatcher.DispatchAsync( + await Assert.ThrowsAsync( + () => dispatcher.DispatchAsync( new PingCommand("x"), - new Session(), + new(), cts.Token ) ); @@ -85,7 +87,7 @@ public async Task RegisterHandler_DisposeToken_StopsDelivery() var token = dispatcher.RegisterHandler(handler); token.Dispose(); - var result = await dispatcher.DispatchAsync(new PingCommand("x"), new Session()); + var result = await dispatcher.DispatchAsync(new PingCommand("x"), new()); Assert.False(result.Matched); Assert.Null(handler.LastText); @@ -97,7 +99,8 @@ public async Task Subscribe_DeliversToDelegate() using var dispatcher = new CommandDispatcher(); string? seen = null; Session? seenContext = null; - dispatcher.Subscribe((command, context, _) => + dispatcher.Subscribe( + (command, context, _) => { seen = command.Text; seenContext = context; @@ -114,9 +117,7 @@ public async Task Subscribe_DeliversToDelegate() Assert.Same(session, seenContext); } - private sealed class Session - { - } + private sealed class Session { } private sealed class RecordingHandler : ICommandHandler { @@ -136,9 +137,7 @@ public Task HandleAsync(PingCommand command, Session context, CancellationToken private sealed class ThrowingHandler : ICommandHandler { public Task HandleAsync(PingCommand command, Session context, CancellationToken cancellationToken = default) - { - throw new InvalidOperationException("Synthetic failure."); - } + => throw new InvalidOperationException("Synthetic failure."); } private sealed record PingCommand(string Text) : ICommand; diff --git a/tests/SquidStd.Tests/Services/Core/EventBusServiceTests.cs b/tests/SquidStd.Tests/Services/Core/EventBusServiceTests.cs index 88ba7b0c..4d74712e 100644 --- a/tests/SquidStd.Tests/Services/Core/EventBusServiceTests.cs +++ b/tests/SquidStd.Tests/Services/Core/EventBusServiceTests.cs @@ -1,6 +1,5 @@ using Serilog; using Serilog.Events; -using SquidStd.Core.Data.Events; using SquidStd.Core.Interfaces.Events; using SquidStd.Services.Core.Services; using SquidStd.Tests.Support; @@ -112,8 +111,7 @@ public async Task PublishAsync_PreCancelledToken_Throws() using var cts = new CancellationTokenSource(); await cts.CancelAsync(); - await Assert.ThrowsAnyAsync(() => bus.PublishAsync(new TestEvent("payload"), cts.Token) - ); + await Assert.ThrowsAnyAsync(() => bus.PublishAsync(new TestEvent("payload"), cts.Token)); } [Fact] @@ -124,8 +122,7 @@ public async Task PublishAsync_ListenerCancellation_IsNotSwallowed() using var cts = new CancellationTokenSource(); bus.RegisterListener(new SelfCancellingListener(cts)); - await Assert.ThrowsAnyAsync(() => bus.PublishAsync(new TestEvent("payload"), cts.Token) - ); + await Assert.ThrowsAnyAsync(() => bus.PublishAsync(new TestEvent("payload"), cts.Token)); } [Fact] @@ -149,7 +146,8 @@ public async Task Subscribe_DelegateHandler_ReceivesAndUnsubscribes() using var eventBus = new EventBusService(); IEventBus bus = eventBus; var received = new List(); - var token = bus.Subscribe((e, _) => + var token = bus.Subscribe( + (e, _) => { received.Add(e.Payload); @@ -228,9 +226,7 @@ public Task HandleAsync(IEvent eventData, CancellationToken cancellationToken = private sealed class ThrowingListener : IEventListener { public Task HandleAsync(TestEvent eventData, CancellationToken cancellationToken = default) - { - throw new InvalidOperationException("listener failure"); - } + => throw new InvalidOperationException("listener failure"); } private sealed class SelfCancellingListener : IEventListener @@ -254,9 +250,7 @@ public Task HandleAsync(TestEvent eventData, CancellationToken cancellationToken private sealed class SlowListener : IEventListener { public async Task HandleAsync(TestEvent eventData, CancellationToken cancellationToken = default) - { - await Task.Delay(TimeSpan.FromMilliseconds(40), cancellationToken); - } + => await Task.Delay(TimeSpan.FromMilliseconds(40), cancellationToken); } [Collection(SerilogEventSinkCollection.Name)] @@ -268,15 +262,15 @@ public async Task PublishAsync_SlowListener_LogsWarning() var sink = new CapturingLogSink(); var original = Log.Logger; Log.Logger = new LoggerConfiguration() - .MinimumLevel.Verbose() - .WriteTo.Sink(sink) - .CreateLogger(); + .MinimumLevel + .Verbose() + .WriteTo + .Sink(sink) + .CreateLogger(); try { - using var eventBus = new EventBusService( - new EventBusOptions { SlowListenerThreshold = TimeSpan.FromMilliseconds(5) } - ); + using var eventBus = new EventBusService(new() { SlowListenerThreshold = TimeSpan.FromMilliseconds(5) }); IEventBus bus = eventBus; bus.RegisterListener(new SlowListener()); diff --git a/tests/SquidStd.Tests/Services/Core/JobSystemServiceTests.cs b/tests/SquidStd.Tests/Services/Core/JobSystemServiceTests.cs index 2b0477b2..18c306c5 100644 --- a/tests/SquidStd.Tests/Services/Core/JobSystemServiceTests.cs +++ b/tests/SquidStd.Tests/Services/Core/JobSystemServiceTests.cs @@ -1,4 +1,3 @@ -using SquidStd.Core.Data.Jobs; using SquidStd.Core.Interfaces.Jobs; using SquidStd.Services.Core.Services; @@ -47,7 +46,8 @@ public async Task ScheduleAsync_ManyJobs_AllComplete() { var value = i; tasks.Add( - system.ScheduleAsync(() => + system.ScheduleAsync( + () => { lock (sync) { @@ -72,8 +72,9 @@ public async Task ScheduleAsync_ThrowingAction_PropagatesExceptionToAwaiter() IJobSystem system = jobs; await jobs.StartAsync(CancellationToken.None); - await Assert.ThrowsAsync(() => - system.ScheduleAsync(() => throw new InvalidOperationException("boom")) + await Assert.ThrowsAsync( + () => + system.ScheduleAsync(() => throw new InvalidOperationException("boom")) ); } @@ -102,7 +103,8 @@ public async Task ScheduleAsync_TokenCancelledBeforePickup_TransitionsToCanceled using var cancellationTokenSource = new CancellationTokenSource(); await jobs.StartAsync(CancellationToken.None); - _ = system.ScheduleAsync(() => + _ = system.ScheduleAsync( + () => { firstStarted.Set(); gate.Wait(TimeSpan.FromSeconds(2)); @@ -127,7 +129,8 @@ public async Task StopAsync_CancelsQueuedJobsAndPreventsNewSchedules() using var firstStarted = new ManualResetEventSlim(false); await jobs.StartAsync(CancellationToken.None); - _ = system.ScheduleAsync(() => + _ = system.ScheduleAsync( + () => { firstStarted.Set(); gate.Wait(TimeSpan.FromSeconds(2)); @@ -140,8 +143,7 @@ public async Task StopAsync_CancelsQueuedJobsAndPreventsNewSchedules() gate.Set(); await Assert.ThrowsAsync(() => queued); - Assert.Throws(() => { _ = system.ScheduleAsync(() => { }); } - ); + Assert.Throws(() => { _ = system.ScheduleAsync(() => { }); }); jobs.Dispose(); } @@ -162,15 +164,13 @@ public void WorkerCount_UsesExplicitValue() } private static JobSystemService NewService(int workerCount) - { - return new JobSystemService( - new JobsConfig + => new( + new() { WorkerThreadCount = workerCount, ShutdownTimeoutSeconds = 1.0 } ); - } // CompletedCount is incremented by the worker after the scheduled task's completion fires, so // awaiting ScheduleAsync does not guarantee the counter is updated yet. Wait for it (bounded). diff --git a/tests/SquidStd.Tests/Services/Core/MainThreadDispatcherServiceTests.cs b/tests/SquidStd.Tests/Services/Core/MainThreadDispatcherServiceTests.cs index 52cd0af7..fb13ca65 100644 --- a/tests/SquidStd.Tests/Services/Core/MainThreadDispatcherServiceTests.cs +++ b/tests/SquidStd.Tests/Services/Core/MainThreadDispatcherServiceTests.cs @@ -13,7 +13,8 @@ public void DrainPending_BudgetExceededAfterFirstCallback_DefersRest() { IMainThreadDispatcher dispatcher = new MainThreadDispatcherService(); var calls = new List(); - dispatcher.Post(() => + dispatcher.Post( + () => { calls.Add(1); Thread.Sleep(5); @@ -34,7 +35,8 @@ public void DrainPending_CallbackPostsAnother_DefersToNextDrain() { IMainThreadDispatcher dispatcher = new MainThreadDispatcherService(); var calls = new List(); - dispatcher.Post(() => + dispatcher.Post( + () => { calls.Add(1); dispatcher.Post(() => calls.Add(2)); diff --git a/tests/SquidStd.Tests/Services/Core/MetricsCollectionServiceTests.cs b/tests/SquidStd.Tests/Services/Core/MetricsCollectionServiceTests.cs index 59346887..30767f10 100644 --- a/tests/SquidStd.Tests/Services/Core/MetricsCollectionServiceTests.cs +++ b/tests/SquidStd.Tests/Services/Core/MetricsCollectionServiceTests.cs @@ -10,7 +10,7 @@ public class MetricsCollectionServiceTests [Fact] public void GetSnapshot_WhenNotStarted_ReturnsEmptySnapshot() { - using var service = new MetricsCollectionService([], new MetricsConfig()); + using var service = new MetricsCollectionService([], new()); var snapshot = service.GetSnapshot(); @@ -23,7 +23,7 @@ public async Task GetStatus_ReturnsLatestMetricsSnapshot() { using var service = new MetricsCollectionService( [new CountingMetricProvider("jobs", "completed.total", 11)], - new MetricsConfig + new() { IntervalMilliseconds = 10, LogEnabled = false @@ -45,7 +45,7 @@ public async Task StartAsync_CollectsMetricsFromRegisteredProviders() { using var service = new MetricsCollectionService( [new CountingMetricProvider("jobs", "pending.total", 7)], - new MetricsConfig + new() { IntervalMilliseconds = 10, LogEnabled = false @@ -74,7 +74,7 @@ public async Task StartAsync_PublishesMetricsCollectedEventThroughEventBus() bus.RegisterListener(secondListener); using var service = new MetricsCollectionService( [new CountingMetricProvider("events", "published.total", 5)], - new MetricsConfig + new() { IntervalMilliseconds = 1000, LogEnabled = false @@ -99,7 +99,7 @@ public async Task StartAsync_WhenDisabled_DoesNotCollectMetrics() var provider = new CountingMetricProvider("jobs", "pending.total", 7); using var service = new MetricsCollectionService( [provider], - new MetricsConfig + new() { Enabled = false, IntervalMilliseconds = 10, @@ -122,7 +122,7 @@ public async Task StartAsync_WhenProviderThrows_ContinuesCollectingOtherProvider new ThrowingMetricProvider("broken"), new CountingMetricProvider("timer", "callbacks.total", 3) ], - new MetricsConfig + new() { IntervalMilliseconds = 10, LogEnabled = false @@ -144,7 +144,7 @@ public async Task StopAsync_StopsCollectionLoop() var provider = new CountingMetricProvider("bus", "dispatch.total", 1); using var service = new MetricsCollectionService( [provider], - new MetricsConfig + new() { IntervalMilliseconds = 10, LogEnabled = false @@ -199,7 +199,7 @@ public ValueTask> CollectAsync(CancellationToken can { Interlocked.Increment(ref _collectionCount); - return ValueTask.FromResult>([new MetricSample(_metricName, _value)]); + return ValueTask.FromResult>([new(_metricName, _value)]); } } @@ -213,9 +213,7 @@ public ThrowingMetricProvider(string providerName) } public ValueTask> CollectAsync(CancellationToken cancellationToken = default) - { - throw new InvalidOperationException("Synthetic test failure."); - } + => throw new InvalidOperationException("Synthetic test failure."); } private sealed class MetricsCollectedListener : IEventListener diff --git a/tests/SquidStd.Tests/Services/Core/RegisterCommandDispatcherExtensionsTests.cs b/tests/SquidStd.Tests/Services/Core/RegisterCommandDispatcherExtensionsTests.cs index e7dbb2ac..2ca8b713 100644 --- a/tests/SquidStd.Tests/Services/Core/RegisterCommandDispatcherExtensionsTests.cs +++ b/tests/SquidStd.Tests/Services/Core/RegisterCommandDispatcherExtensionsTests.cs @@ -40,9 +40,7 @@ public async Task Activator_SubscribesRegisteredHandlers() Assert.Equal("hi", container.Resolve().LastText); } - private sealed class Session - { - } + private sealed class Session { } private sealed class PingHandler : ICommandHandler { diff --git a/tests/SquidStd.Tests/Services/Core/Scheduling/CronSchedulerServiceTests.cs b/tests/SquidStd.Tests/Services/Core/Scheduling/CronSchedulerServiceTests.cs index ff7ea983..02ee3891 100644 --- a/tests/SquidStd.Tests/Services/Core/Scheduling/CronSchedulerServiceTests.cs +++ b/tests/SquidStd.Tests/Services/Core/Scheduling/CronSchedulerServiceTests.cs @@ -1,5 +1,4 @@ using Cronos; -using SquidStd.Core.Data.Timing; using SquidStd.Services.Core.Services; using SquidStd.Services.Core.Services.Scheduling; using SquidStd.Tests.Support; @@ -104,7 +103,7 @@ public void Jobs_ExposesRegisteredJob() public void RealTimerWheel_FiresJob_WhenAdvancedPastAMinute() { var timer = new TimerWheelService( - new TimerWheelConfig + new() { TickDuration = TimeSpan.FromMilliseconds(8), WheelSize = 512 diff --git a/tests/SquidStd.Tests/Services/Core/Scheduling/TimerWheelPumpServiceTests.cs b/tests/SquidStd.Tests/Services/Core/Scheduling/TimerWheelPumpServiceTests.cs index e2dc7208..ae020365 100644 --- a/tests/SquidStd.Tests/Services/Core/Scheduling/TimerWheelPumpServiceTests.cs +++ b/tests/SquidStd.Tests/Services/Core/Scheduling/TimerWheelPumpServiceTests.cs @@ -1,4 +1,3 @@ -using SquidStd.Core.Data.Timing; using SquidStd.Services.Core.Services.Scheduling; using SquidStd.Tests.Support; @@ -8,13 +7,12 @@ public class TimerWheelPumpServiceTests { [Fact] public void Ctor_NonPositiveInterval_Throws() - { - Assert.Throws(() => new TimerWheelPumpService( + => Assert.Throws( + () => new TimerWheelPumpService( new FakeTimerService(), - new TimerWheelPumpConfig { PumpInterval = TimeSpan.Zero } + new() { PumpInterval = TimeSpan.Zero } ) ); - } [Fact] public async Task Pump_AdvancesTheWheel() @@ -22,7 +20,7 @@ public async Task Pump_AdvancesTheWheel() var timer = new FakeTimerService(); var pump = new TimerWheelPumpService( timer, - new TimerWheelPumpConfig { PumpInterval = TimeSpan.FromMilliseconds(20) } + new() { PumpInterval = TimeSpan.FromMilliseconds(20) } ); await pump.StartAsync(); diff --git a/tests/SquidStd.Tests/Services/Core/SeededCommandDispatcherTests.cs b/tests/SquidStd.Tests/Services/Core/SeededCommandDispatcherTests.cs index 8ec0a1b2..9cb55b3a 100644 --- a/tests/SquidStd.Tests/Services/Core/SeededCommandDispatcherTests.cs +++ b/tests/SquidStd.Tests/Services/Core/SeededCommandDispatcherTests.cs @@ -16,7 +16,7 @@ public async Task DispatchAsync_BuildsContextFromSeed() inner.RegisterHandler(handler); var seeded = new SeededCommandDispatcher(inner, new ConnectionSessionFactory()); - var result = await seeded.DispatchAsync(new PingCommand("hi"), new Connection("conn-1")); + var result = await seeded.DispatchAsync(new PingCommand("hi"), new("conn-1")); Assert.True(result.Matched); Assert.Equal(1, result.HandlerCount); @@ -35,7 +35,7 @@ public async Task RegisterSeededCommandDispatcher_ResolvesAndDispatches() await container.Resolve>().StartAsync(CancellationToken.None); var seeded = container.Resolve>(); - var result = await seeded.DispatchAsync(new PingCommand("yo"), new Connection("conn-2")); + var result = await seeded.DispatchAsync(new PingCommand("yo"), new("conn-2")); Assert.True(result.Matched); var handler = container.Resolve(); @@ -66,9 +66,7 @@ public Session(string id) private sealed class ConnectionSessionFactory : ICommandContextFactory { public Session Create(Connection seed) - { - return new Session(seed.Id); - } + => new(seed.Id); } private sealed class RecordingHandler : ICommandHandler diff --git a/tests/SquidStd.Tests/Services/Core/SquidStdLogRollingIntervalExtensionsTests.cs b/tests/SquidStd.Tests/Services/Core/SquidStdLogRollingIntervalExtensionsTests.cs index 28edec22..900c5a58 100644 --- a/tests/SquidStd.Tests/Services/Core/SquidStdLogRollingIntervalExtensionsTests.cs +++ b/tests/SquidStd.Tests/Services/Core/SquidStdLogRollingIntervalExtensionsTests.cs @@ -6,24 +6,19 @@ namespace SquidStd.Tests.Services.Core; public class SquidStdLogRollingIntervalExtensionsTests { - [Theory] - [InlineData(SquidStdLogRollingIntervalType.Infinite, RollingInterval.Infinite)] - [InlineData(SquidStdLogRollingIntervalType.Year, RollingInterval.Year)] - [InlineData(SquidStdLogRollingIntervalType.Month, RollingInterval.Month)] - [InlineData(SquidStdLogRollingIntervalType.Day, RollingInterval.Day)] - [InlineData(SquidStdLogRollingIntervalType.Hour, RollingInterval.Hour)] - [InlineData(SquidStdLogRollingIntervalType.Minute, RollingInterval.Minute)] + [Theory, InlineData(SquidStdLogRollingIntervalType.Infinite, RollingInterval.Infinite), + InlineData(SquidStdLogRollingIntervalType.Year, RollingInterval.Year), + InlineData(SquidStdLogRollingIntervalType.Month, RollingInterval.Month), + InlineData(SquidStdLogRollingIntervalType.Day, RollingInterval.Day), + InlineData(SquidStdLogRollingIntervalType.Hour, RollingInterval.Hour), + InlineData(SquidStdLogRollingIntervalType.Minute, RollingInterval.Minute)] public void ToSerilogRollingInterval_KnownIntervals_MapExpected( SquidStdLogRollingIntervalType input, RollingInterval expected ) - { - Assert.Equal(expected, input.ToSerilogRollingInterval()); - } + => Assert.Equal(expected, input.ToSerilogRollingInterval()); [Fact] public void ToSerilogRollingInterval_UnmappedInterval_FallsBackToDay() - { - Assert.Equal(RollingInterval.Day, ((SquidStdLogRollingIntervalType)255).ToSerilogRollingInterval()); - } + => Assert.Equal(RollingInterval.Day, ((SquidStdLogRollingIntervalType)255).ToSerilogRollingInterval()); } diff --git a/tests/SquidStd.Tests/Services/Core/TimerWheelServiceTests.cs b/tests/SquidStd.Tests/Services/Core/TimerWheelServiceTests.cs index d8e3947f..ca10cc8b 100644 --- a/tests/SquidStd.Tests/Services/Core/TimerWheelServiceTests.cs +++ b/tests/SquidStd.Tests/Services/Core/TimerWheelServiceTests.cs @@ -1,4 +1,3 @@ -using SquidStd.Core.Data.Timing; using SquidStd.Core.Interfaces.Timing; using SquidStd.Services.Core.Services; @@ -23,11 +22,13 @@ public void CallbackException_DoesNotStopOtherTimers() [Fact] public void Ctor_InvalidConfig_Throws() { - Assert.Throws(() => - new TimerWheelService(new TimerWheelConfig { TickDuration = TimeSpan.Zero }) + Assert.Throws( + () => + new TimerWheelService(new() { TickDuration = TimeSpan.Zero }) ); - Assert.Throws(() => - new TimerWheelService(new TimerWheelConfig { TickDuration = TimeSpan.FromMilliseconds(8), WheelSize = 0 }) + Assert.Throws( + () => + new TimerWheelService(new() { TickDuration = TimeSpan.FromMilliseconds(8), WheelSize = 0 }) ); } @@ -152,13 +153,11 @@ public void UpdateTicksDelta_AdvancesByWholeTicks() } private static TimerWheelService NewService(int tickDurationMs = 8, int wheelSize = 16) - { - return new TimerWheelService( - new TimerWheelConfig + => new( + new() { TickDuration = TimeSpan.FromMilliseconds(tickDurationMs), WheelSize = wheelSize } ); - } } diff --git a/tests/SquidStd.Tests/SquidStd.Tests.csproj b/tests/SquidStd.Tests/SquidStd.Tests.csproj index 70fb3401..69fab085 100644 --- a/tests/SquidStd.Tests/SquidStd.Tests.csproj +++ b/tests/SquidStd.Tests/SquidStd.Tests.csproj @@ -8,73 +8,73 @@ - - - - - - - - - - - - + + + + + + + + + + + + - + - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/SquidStd.Tests/Storage/FileStorageServiceTests.cs b/tests/SquidStd.Tests/Storage/FileStorageServiceTests.cs index 2254f41f..5a12f47d 100644 --- a/tests/SquidStd.Tests/Storage/FileStorageServiceTests.cs +++ b/tests/SquidStd.Tests/Storage/FileStorageServiceTests.cs @@ -1,5 +1,4 @@ using System.Text; -using SquidStd.Storage.Abstractions.Data.Config; using SquidStd.Storage.Services; using SquidStd.Tests.Support; @@ -11,7 +10,7 @@ public class FileStorageServiceTests public async Task DeleteAsync_RemovesStoredValue() { using var temp = new TempDirectory(); - var service = new FileStorageService(new StorageConfig { RootDirectory = temp.Path }); + var service = new FileStorageService(new() { RootDirectory = temp.Path }); await service.SaveAsync("cache/value.bin", new byte[] { 1, 2, 3 }); var deleted = await service.DeleteAsync("cache/value.bin"); @@ -25,7 +24,7 @@ public async Task DeleteAsync_RemovesStoredValue() public async Task ListKeysAsync_EmptyStore_ReturnsEmpty() { using var temp = new TempDirectory(); - var service = new FileStorageService(new StorageConfig { RootDirectory = temp.Path }); + var service = new FileStorageService(new() { RootDirectory = temp.Path }); Assert.Empty(await ToListAsync(service.ListKeysAsync())); } @@ -34,7 +33,7 @@ public async Task ListKeysAsync_EmptyStore_ReturnsEmpty() public async Task ListKeysAsync_ReturnsSavedKeys_AndFiltersByPrefix() { using var temp = new TempDirectory(); - var service = new FileStorageService(new StorageConfig { RootDirectory = temp.Path }); + var service = new FileStorageService(new() { RootDirectory = temp.Path }); await service.SaveAsync("a/one.bin", new byte[] { 1 }); await service.SaveAsync("a/two.bin", new byte[] { 2 }); await service.SaveAsync("b/three.bin", new byte[] { 3 }); @@ -50,7 +49,7 @@ public async Task ListKeysAsync_ReturnsSavedKeys_AndFiltersByPrefix() public async Task ObjectStorage_ListKeysAsync_ReturnsSavedKeys() { using var temp = new TempDirectory(); - var storage = new FileStorageService(new StorageConfig { RootDirectory = temp.Path }); + var storage = new FileStorageService(new() { RootDirectory = temp.Path }); var objects = new YamlObjectStorageService(storage); await objects.SaveAsync("objects/x.yaml", new SampleObject { Name = "x", Value = 1 }); @@ -63,7 +62,7 @@ public async Task ObjectStorage_ListKeysAsync_ReturnsSavedKeys() public async Task ObjectStorage_SaveAsync_LoadAsync_RoundTripsYamlObject() { using var temp = new TempDirectory(); - var storage = new FileStorageService(new StorageConfig { RootDirectory = temp.Path }); + var storage = new FileStorageService(new() { RootDirectory = temp.Path }); var objects = new YamlObjectStorageService(storage); var expected = new SampleObject { @@ -84,7 +83,7 @@ public async Task ObjectStorage_SaveAsync_LoadAsync_RoundTripsYamlObject() public async Task SaveAsync_LoadAsync_RoundTripsBytes() { using var temp = new TempDirectory(); - var service = new FileStorageService(new StorageConfig { RootDirectory = temp.Path }); + var service = new FileStorageService(new() { RootDirectory = temp.Path }); var data = Encoding.UTF8.GetBytes("hello storage"); await service.SaveAsync("profiles/main.bin", data); @@ -95,14 +94,11 @@ public async Task SaveAsync_LoadAsync_RoundTripsBytes() Assert.True(await service.ExistsAsync("profiles/main.bin")); } - [Theory] - [InlineData("../escape.bin")] - [InlineData("/absolute.bin")] - [InlineData("nested/../../escape.bin")] + [Theory, InlineData("../escape.bin"), InlineData("/absolute.bin"), InlineData("nested/../../escape.bin")] public async Task SaveAsync_RejectsUnsafeKeys(string key) { using var temp = new TempDirectory(); - var service = new FileStorageService(new StorageConfig { RootDirectory = temp.Path }); + var service = new FileStorageService(new() { RootDirectory = temp.Path }); await Assert.ThrowsAsync(() => service.SaveAsync(key, new byte[] { 1 }).AsTask()); } diff --git a/tests/SquidStd.Tests/Storage/S3/MinioContainerFixture.cs b/tests/SquidStd.Tests/Storage/S3/MinioContainerFixture.cs index 368a5826..f7dda433 100644 --- a/tests/SquidStd.Tests/Storage/S3/MinioContainerFixture.cs +++ b/tests/SquidStd.Tests/Storage/S3/MinioContainerFixture.cs @@ -3,7 +3,7 @@ namespace SquidStd.Tests.Storage.S3; /// -/// Starts a MinIO container once for the whole collection and exposes its endpoint and credentials. +/// Starts a MinIO container once for the whole collection and exposes its endpoint and credentials. /// public sealed class MinioContainerFixture : IAsyncLifetime { @@ -18,14 +18,10 @@ public sealed class MinioContainerFixture : IAsyncLifetime public string SecretKey => _container.GetSecretKey(); public Task DisposeAsync() - { - return _container.DisposeAsync().AsTask(); - } + => _container.DisposeAsync().AsTask(); public Task InitializeAsync() - { - return _container.StartAsync(); - } + => _container.StartAsync(); } [CollectionDefinition(Name)] diff --git a/tests/SquidStd.Tests/Storage/S3/S3StorageServiceTests.cs b/tests/SquidStd.Tests/Storage/S3/S3StorageServiceTests.cs index cf36ea79..7c1443b0 100644 --- a/tests/SquidStd.Tests/Storage/S3/S3StorageServiceTests.cs +++ b/tests/SquidStd.Tests/Storage/S3/S3StorageServiceTests.cs @@ -1,6 +1,4 @@ using System.Text; -using SquidStd.Aws.Abstractions.Data.Config; -using SquidStd.Storage.S3.Data.Config; using SquidStd.Storage.S3.Services; namespace SquidStd.Tests.Storage.S3; @@ -17,13 +15,10 @@ public S3StorageServiceTests(MinioContainerFixture fixture) [Fact] public void Ctor_MissingServiceUrl_Throws() - { - Assert.ThrowsAny(() => - new S3StorageService( - new S3StorageOptions { Aws = new AwsConfigEntry { AccessKey = "a", SecretKey = "b" }, Bucket = "c" } - ) + => Assert.ThrowsAny( + () => + new S3StorageService(new() { Aws = new() { AccessKey = "a", SecretKey = "b" }, Bucket = "c" }) ); - } [Fact] public async Task Exists_And_Delete() @@ -60,9 +55,7 @@ public async Task ListKeysAsync_ReturnsObjectsUnderPrefix() [Fact] public async Task Load_MissingKey_ReturnsNull() - { - Assert.Null(await NewService().LoadAsync(Key())); - } + => Assert.Null(await NewService().LoadAsync(Key())); [Fact] public async Task SaveThenLoad_RoundTrips_AndCreatesBucket() @@ -78,16 +71,13 @@ public async Task SaveThenLoad_RoundTrips_AndCreatesBucket() } private static string Key() - { - return "k-" + Guid.NewGuid().ToString("N"); - } + => "k-" + Guid.NewGuid().ToString("N"); private S3StorageService NewService() - { - return new S3StorageService( - new S3StorageOptions + => new( + new() { - Aws = new AwsConfigEntry + Aws = new() { ServiceUrl = _fixture.ServiceUrl, AccessKey = _fixture.AccessKey, @@ -96,5 +86,4 @@ private S3StorageService NewService() Bucket = "squidstd-tests" } ); - } } diff --git a/tests/SquidStd.Tests/Storage/StorageRegistrationTests.cs b/tests/SquidStd.Tests/Storage/StorageRegistrationTests.cs index 6d35c2fd..a7d7f88c 100644 --- a/tests/SquidStd.Tests/Storage/StorageRegistrationTests.cs +++ b/tests/SquidStd.Tests/Storage/StorageRegistrationTests.cs @@ -1,5 +1,4 @@ using DryIoc; -using SquidStd.Storage.Abstractions.Data.Config; using SquidStd.Storage.Abstractions.Interfaces; using SquidStd.Storage.Extensions; @@ -13,7 +12,7 @@ public async Task AddFileStorage_ResolvesAndRoundTrips() var root = Path.Combine(Path.GetTempPath(), "squidstd-storage-" + Guid.NewGuid().ToString("N")); using var container = new Container(); - container.AddFileStorage(new StorageConfig { RootDirectory = root }); + container.AddFileStorage(new() { RootDirectory = root }); var storage = container.Resolve(); Assert.NotNull(container.Resolve()); diff --git a/tests/SquidStd.Tests/Support/AppendingMiddleware.cs b/tests/SquidStd.Tests/Support/AppendingMiddleware.cs index a3578f8f..fc63af15 100644 --- a/tests/SquidStd.Tests/Support/AppendingMiddleware.cs +++ b/tests/SquidStd.Tests/Support/AppendingMiddleware.cs @@ -4,8 +4,8 @@ namespace SquidStd.Tests.Support; /// -/// Test middleware that appends a marker byte to every payload, on both receive and send paths. -/// Used to verify pipeline ordering and transformation. +/// Test middleware that appends a marker byte to every payload, on both receive and send paths. +/// Used to verify pipeline ordering and transformation. /// public sealed class AppendingMiddleware : INetMiddleware { @@ -21,18 +21,14 @@ public ValueTask> ProcessAsync( ReadOnlyMemory data, CancellationToken cancellationToken = default ) - { - return ValueTask.FromResult>(Append(data)); - } + => ValueTask.FromResult>(Append(data)); public ValueTask> ProcessSendAsync( SquidStdTcpClient? client, ReadOnlyMemory data, CancellationToken cancellationToken = default ) - { - return ValueTask.FromResult>(Append(data)); - } + => ValueTask.FromResult>(Append(data)); private byte[] Append(ReadOnlyMemory data) { diff --git a/tests/SquidStd.Tests/Support/CapturingLogSink.cs b/tests/SquidStd.Tests/Support/CapturingLogSink.cs index d0beaf98..f2232b82 100644 --- a/tests/SquidStd.Tests/Support/CapturingLogSink.cs +++ b/tests/SquidStd.Tests/Support/CapturingLogSink.cs @@ -4,7 +4,7 @@ namespace SquidStd.Tests.Support; /// -/// In-memory Serilog sink that records emitted log events for assertions. +/// In-memory Serilog sink that records emitted log events for assertions. /// public sealed class CapturingLogSink : ILogEventSink { diff --git a/tests/SquidStd.Tests/Support/CountingXorCodec.cs b/tests/SquidStd.Tests/Support/CountingXorCodec.cs index 150defb6..3d1939f0 100644 --- a/tests/SquidStd.Tests/Support/CountingXorCodec.cs +++ b/tests/SquidStd.Tests/Support/CountingXorCodec.cs @@ -3,9 +3,9 @@ namespace SquidStd.Tests.Support; /// -/// Deterministic stateful test codec: XORs each byte with a counter-based keystream (seed + position). -/// Decode and Encode keep independent positions, mirroring the separate receive/send state of real -/// stream ciphers. Two codecs created with the same seed cancel out (one encodes, the other decodes). +/// Deterministic stateful test codec: XORs each byte with a counter-based keystream (seed + position). +/// Decode and Encode keep independent positions, mirroring the separate receive/send state of real +/// stream ciphers. Two codecs created with the same seed cancel out (one encodes, the other decodes). /// public sealed class CountingXorCodec : ITransportCodec { diff --git a/tests/SquidStd.Tests/Support/DroppingMiddleware.cs b/tests/SquidStd.Tests/Support/DroppingMiddleware.cs index 833f7002..448a5063 100644 --- a/tests/SquidStd.Tests/Support/DroppingMiddleware.cs +++ b/tests/SquidStd.Tests/Support/DroppingMiddleware.cs @@ -4,8 +4,8 @@ namespace SquidStd.Tests.Support; /// -/// Test middleware that drops every payload by returning , -/// short-circuiting the pipeline. +/// Test middleware that drops every payload by returning , +/// short-circuiting the pipeline. /// public sealed class DroppingMiddleware : INetMiddleware { @@ -14,16 +14,12 @@ public ValueTask> ProcessAsync( ReadOnlyMemory data, CancellationToken cancellationToken = default ) - { - return ValueTask.FromResult(ReadOnlyMemory.Empty); - } + => ValueTask.FromResult(ReadOnlyMemory.Empty); public ValueTask> ProcessSendAsync( SquidStdTcpClient? client, ReadOnlyMemory data, CancellationToken cancellationToken = default ) - { - return ValueTask.FromResult(ReadOnlyMemory.Empty); - } + => ValueTask.FromResult(ReadOnlyMemory.Empty); } diff --git a/tests/SquidStd.Tests/Support/DummyGuidConverter.cs b/tests/SquidStd.Tests/Support/DummyGuidConverter.cs index 219e12d5..e989ec5f 100644 --- a/tests/SquidStd.Tests/Support/DummyGuidConverter.cs +++ b/tests/SquidStd.Tests/Support/DummyGuidConverter.cs @@ -4,17 +4,13 @@ namespace SquidStd.Tests.Support; /// -/// No-op JSON converter used to exercise converter registration helpers. +/// No-op JSON converter used to exercise converter registration helpers. /// public class DummyGuidConverter : JsonConverter { public override Guid Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - return Guid.Empty; - } + => Guid.Empty; public override void Write(Utf8JsonWriter writer, Guid value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.ToString()); - } + => writer.WriteStringValue(value.ToString()); } diff --git a/tests/SquidStd.Tests/Support/FakeCacheProvider.cs b/tests/SquidStd.Tests/Support/FakeCacheProvider.cs index 2fc4bd35..d2f71128 100644 --- a/tests/SquidStd.Tests/Support/FakeCacheProvider.cs +++ b/tests/SquidStd.Tests/Support/FakeCacheProvider.cs @@ -4,7 +4,7 @@ namespace SquidStd.Tests.Support; /// -/// In-memory for tests. Ignores TTL expiry (records the last TTL seen). +/// In-memory for tests. Ignores TTL expiry (records the last TTL seen). /// public sealed class FakeCacheProvider : ICacheProvider { @@ -13,9 +13,7 @@ public sealed class FakeCacheProvider : ICacheProvider public TimeSpan? LastTtl { get; private set; } public Task ExistsAsync(string key, CancellationToken cancellationToken = default) - { - return Task.FromResult(_store.ContainsKey(key)); - } + => Task.FromResult(_store.ContainsKey(key)); public Task?> GetAsync(string key, CancellationToken cancellationToken = default) { @@ -28,9 +26,7 @@ public Task ExistsAsync(string key, CancellationToken cancellationToken = } public Task RemoveAsync(string key, CancellationToken cancellationToken = default) - { - return Task.FromResult(_store.TryRemove(key, out _)); - } + => Task.FromResult(_store.TryRemove(key, out _)); public Task SetAsync( string key, @@ -46,12 +42,8 @@ public Task SetAsync( } public ValueTask StartAsync(CancellationToken cancellationToken = default) - { - return ValueTask.CompletedTask; - } + => ValueTask.CompletedTask; public ValueTask StopAsync(CancellationToken cancellationToken = default) - { - return ValueTask.CompletedTask; - } + => ValueTask.CompletedTask; } diff --git a/tests/SquidStd.Tests/Support/FakeHealthCheck.cs b/tests/SquidStd.Tests/Support/FakeHealthCheck.cs index 108c1d8d..7def6254 100644 --- a/tests/SquidStd.Tests/Support/FakeHealthCheck.cs +++ b/tests/SquidStd.Tests/Support/FakeHealthCheck.cs @@ -4,8 +4,8 @@ namespace SquidStd.Tests.Support; /// -/// Configurable for tests: returns a fixed result, optionally after a -/// delay, or throws a configured exception. +/// Configurable for tests: returns a fixed result, optionally after a +/// delay, or throws a configured exception. /// public sealed class FakeHealthCheck : IHealthCheck { diff --git a/tests/SquidStd.Tests/Support/FakeNetworkConnection.cs b/tests/SquidStd.Tests/Support/FakeNetworkConnection.cs index 77e24963..fe634f36 100644 --- a/tests/SquidStd.Tests/Support/FakeNetworkConnection.cs +++ b/tests/SquidStd.Tests/Support/FakeNetworkConnection.cs @@ -4,8 +4,8 @@ namespace SquidStd.Tests.Support; /// -/// In-memory for testing sessions without sockets. -/// Records sent payloads and close calls; SendCallback can inject failures. +/// In-memory for testing sessions without sockets. +/// Records sent payloads and close calls; SendCallback can inject failures. /// public sealed class FakeNetworkConnection : INetworkConnection { diff --git a/tests/SquidStd.Tests/Support/FakePlugin.cs b/tests/SquidStd.Tests/Support/FakePlugin.cs index 5f9934d5..a5571283 100644 --- a/tests/SquidStd.Tests/Support/FakePlugin.cs +++ b/tests/SquidStd.Tests/Support/FakePlugin.cs @@ -5,23 +5,23 @@ namespace SquidStd.Tests.Support; /// -/// Minimal implementation used to exercise the plugin contract. +/// Minimal implementation used to exercise the plugin contract. /// public class FakePlugin : ISquidStdPlugin { /// - /// Gets the context received during the last call. + /// Gets the context received during the last call. /// public PluginContext? ReceivedContext { get; private set; } /// - /// Gets the plugin metadata. + /// Gets the plugin metadata. /// public PluginMetadata Metadata { get; } = new() { Id = "squidstd.fake", Name = "Fake Plugin", - Version = new Version(1, 2, 3), + Version = new(1, 2, 3), Author = "tests" }; diff --git a/tests/SquidStd.Tests/Support/FakeStdService.cs b/tests/SquidStd.Tests/Support/FakeStdService.cs index f6cb6261..24e464d0 100644 --- a/tests/SquidStd.Tests/Support/FakeStdService.cs +++ b/tests/SquidStd.Tests/Support/FakeStdService.cs @@ -3,12 +3,12 @@ namespace SquidStd.Tests.Support; /// -/// Minimal implementation tracking start/stop calls. +/// Minimal implementation tracking start/stop calls. /// public class FakeStdService : ISquidStdService { /// - /// Gets a value indicating whether the service is currently started. + /// Gets a value indicating whether the service is currently started. /// public bool Started { get; private set; } diff --git a/tests/SquidStd.Tests/Support/FakeTimeProvider.cs b/tests/SquidStd.Tests/Support/FakeTimeProvider.cs index b9df069d..be2815a6 100644 --- a/tests/SquidStd.Tests/Support/FakeTimeProvider.cs +++ b/tests/SquidStd.Tests/Support/FakeTimeProvider.cs @@ -1,8 +1,8 @@ namespace SquidStd.Tests.Support; /// -/// Test TimeProvider with a manually advanced clock and an inert timer (so periodic sweeps -/// never fire on their own; tests invoke the sweep explicitly). +/// Test TimeProvider with a manually advanced clock and an inert timer (so periodic sweeps +/// never fire on their own; tests invoke the sweep explicitly). /// public sealed class FakeTimeProvider : TimeProvider { @@ -14,34 +14,22 @@ public FakeTimeProvider(DateTimeOffset start) } public void Advance(TimeSpan delta) - { - _utcNow = _utcNow.Add(delta); - } + => _utcNow = _utcNow.Add(delta); public override ITimer CreateTimer(TimerCallback callback, object? state, TimeSpan dueTime, TimeSpan period) - { - return new InertTimer(); - } + => new InertTimer(); public override DateTimeOffset GetUtcNow() - { - return _utcNow; - } + => _utcNow; private sealed class InertTimer : ITimer { public bool Change(TimeSpan dueTime, TimeSpan period) - { - return true; - } + => true; public ValueTask DisposeAsync() - { - return ValueTask.CompletedTask; - } + => ValueTask.CompletedTask; - public void Dispose() - { - } + public void Dispose() { } } } diff --git a/tests/SquidStd.Tests/Support/FakeTimerService.cs b/tests/SquidStd.Tests/Support/FakeTimerService.cs index fea9a939..be9668a5 100644 --- a/tests/SquidStd.Tests/Support/FakeTimerService.cs +++ b/tests/SquidStd.Tests/Support/FakeTimerService.cs @@ -3,9 +3,9 @@ namespace SquidStd.Tests.Support; /// -/// In-memory for tests. Timers do not fire on their own: -/// call to invoke and clear the currently-registered timers -/// (one-shot semantics). only records that a pump ran. +/// In-memory for tests. Timers do not fire on their own: +/// call to invoke and clear the currently-registered timers +/// (one-shot semantics). only records that a pump ran. /// public sealed class FakeTimerService : ITimerService { @@ -26,14 +26,10 @@ public string RegisterTimer(string name, TimeSpan interval, Action callback, Tim } public void UnregisterAllTimers() - { - _timers.Clear(); - } + => _timers.Clear(); public bool UnregisterTimer(string timerId) - { - return _timers.Remove(timerId); - } + => _timers.Remove(timerId); public int UnregisterTimersByName(string name) { diff --git a/tests/SquidStd.Tests/Support/LengthPrefixFramer.cs b/tests/SquidStd.Tests/Support/LengthPrefixFramer.cs index 90d40464..a58fe544 100644 --- a/tests/SquidStd.Tests/Support/LengthPrefixFramer.cs +++ b/tests/SquidStd.Tests/Support/LengthPrefixFramer.cs @@ -3,8 +3,8 @@ namespace SquidStd.Tests.Support; /// -/// Test framer: a single length-prefix byte followed by that many payload bytes. The emitted frame -/// length includes the prefix byte. +/// Test framer: a single length-prefix byte followed by that many payload bytes. The emitted frame +/// length includes the prefix byte. /// public sealed class LengthPrefixFramer : INetFramer { diff --git a/tests/SquidStd.Tests/Support/ManualJobSystem.cs b/tests/SquidStd.Tests/Support/ManualJobSystem.cs index 54387c49..01c988e6 100644 --- a/tests/SquidStd.Tests/Support/ManualJobSystem.cs +++ b/tests/SquidStd.Tests/Support/ManualJobSystem.cs @@ -3,9 +3,9 @@ namespace SquidStd.Tests.Support; /// -/// Single-threaded for tests: -/// queues the work; call to execute it. This keeps overlap and -/// rescheduling tests fully deterministic. +/// Single-threaded for tests: +/// queues the work; call to execute it. This keeps overlap and +/// rescheduling tests fully deterministic. /// public sealed class ManualJobSystem : IJobSystem { @@ -49,7 +49,5 @@ public int RunAll() return snapshot.Length; } - public void Dispose() - { - } + public void Dispose() { } } diff --git a/tests/SquidStd.Tests/Support/OtherDto.cs b/tests/SquidStd.Tests/Support/OtherDto.cs index 4aa40f90..d3acf7c3 100644 --- a/tests/SquidStd.Tests/Support/OtherDto.cs +++ b/tests/SquidStd.Tests/Support/OtherDto.cs @@ -1,12 +1,12 @@ namespace SquidStd.Tests.Support; /// -/// Secondary data carrier used to verify multi-type JSON serializer contexts. +/// Secondary data carrier used to verify multi-type JSON serializer contexts. /// public class OtherDto { /// - /// Gets or sets the value. + /// Gets or sets the value. /// public string Value { get; set; } = ""; } diff --git a/tests/SquidStd.Tests/Support/SampleDto.cs b/tests/SquidStd.Tests/Support/SampleDto.cs index f3442f31..e20b9f7c 100644 --- a/tests/SquidStd.Tests/Support/SampleDto.cs +++ b/tests/SquidStd.Tests/Support/SampleDto.cs @@ -1,17 +1,17 @@ namespace SquidStd.Tests.Support; /// -/// Simple data carrier used for JSON and YAML serialization round-trip tests. +/// Simple data carrier used for JSON and YAML serialization round-trip tests. /// public class SampleDto { /// - /// Gets or sets the display name. + /// Gets or sets the display name. /// public string Name { get; set; } = ""; /// - /// Gets or sets the numeric count. + /// Gets or sets the numeric count. /// public int Count { get; set; } } diff --git a/tests/SquidStd.Tests/Support/SerilogEventSinkCollection.cs b/tests/SquidStd.Tests/Support/SerilogEventSinkCollection.cs index 808d4c74..a37b1d3b 100644 --- a/tests/SquidStd.Tests/Support/SerilogEventSinkCollection.cs +++ b/tests/SquidStd.Tests/Support/SerilogEventSinkCollection.cs @@ -1,8 +1,8 @@ namespace SquidStd.Tests.Support; /// -/// Serializes the tests that mutate the static EventSink.OnLogReceived handler, -/// preventing cross-talk between parallel test classes. +/// Serializes the tests that mutate the static EventSink.OnLogReceived handler, +/// preventing cross-talk between parallel test classes. /// [CollectionDefinition(Name, DisableParallelization = true)] public class SerilogEventSinkCollection diff --git a/tests/SquidStd.Tests/Support/TempDirectory.cs b/tests/SquidStd.Tests/Support/TempDirectory.cs index 65f1d40d..931f7259 100644 --- a/tests/SquidStd.Tests/Support/TempDirectory.cs +++ b/tests/SquidStd.Tests/Support/TempDirectory.cs @@ -3,12 +3,12 @@ namespace SquidStd.Tests.Support; /// -/// Creates a unique temporary directory for a test and removes it on dispose. +/// Creates a unique temporary directory for a test and removes it on dispose. /// public sealed class TempDirectory : IDisposable { /// - /// Gets the absolute path of the temporary directory. + /// Gets the absolute path of the temporary directory. /// public string Path { get; } @@ -19,14 +19,12 @@ public TempDirectory() } /// - /// Combines a relative path with the temporary directory root. + /// Combines a relative path with the temporary directory root. /// /// The relative path. /// The combined absolute path. public string Combine(string relative) - { - return SysPath.Combine(Path, relative); - } + => SysPath.Combine(Path, relative); public void Dispose() { diff --git a/tests/SquidStd.Tests/Support/TestDirectoryType.cs b/tests/SquidStd.Tests/Support/TestDirectoryType.cs index 588a42ea..12b64303 100644 --- a/tests/SquidStd.Tests/Support/TestDirectoryType.cs +++ b/tests/SquidStd.Tests/Support/TestDirectoryType.cs @@ -1,7 +1,7 @@ namespace SquidStd.Tests.Support; /// -/// Directory type values used to exercise DirectoriesConfig enum overloads. +/// Directory type values used to exercise DirectoriesConfig enum overloads. /// public enum TestDirectoryType { diff --git a/tests/SquidStd.Tests/Support/TestJsonContext.cs b/tests/SquidStd.Tests/Support/TestJsonContext.cs index 7f738a2a..f6c211e3 100644 --- a/tests/SquidStd.Tests/Support/TestJsonContext.cs +++ b/tests/SquidStd.Tests/Support/TestJsonContext.cs @@ -3,10 +3,7 @@ namespace SquidStd.Tests.Support; /// -/// Source-generated JSON serializer context exposing the test DTO types. +/// Source-generated JSON serializer context exposing the test DTO types. /// -[JsonSerializable(typeof(SampleDto))] -[JsonSerializable(typeof(OtherDto))] -public partial class TestJsonContext : JsonSerializerContext -{ -} +[JsonSerializable(typeof(SampleDto)), JsonSerializable(typeof(OtherDto))] +public partial class TestJsonContext : JsonSerializerContext { } diff --git a/tests/SquidStd.Tests/Telemetry/MetricsSnapshotBridgeTests.cs b/tests/SquidStd.Tests/Telemetry/MetricsSnapshotBridgeTests.cs index 7482efe7..4e4cca5d 100644 --- a/tests/SquidStd.Tests/Telemetry/MetricsSnapshotBridgeTests.cs +++ b/tests/SquidStd.Tests/Telemetry/MetricsSnapshotBridgeTests.cs @@ -25,9 +25,9 @@ public void Bridge_ExportsSnapshotSamplesAsInstruments() using var bridge = new MetricsSnapshotBridge(metrics); using (var provider = Sdk.CreateMeterProviderBuilder() - .AddMeter(MetricsSnapshotBridge.MeterName) - .AddInMemoryExporter(exported) - .Build()) + .AddMeter(MetricsSnapshotBridge.MeterName) + .AddInMemoryExporter(exported) + .Build()) { provider.ForceFlush(); } @@ -43,8 +43,8 @@ private static double ReadValue(List metrics, string name) foreach (ref readonly var point in metric.GetMetricPoints()) { return metric.MetricType == OtelMetricType.DoubleGauge - ? point.GetGaugeLastValueDouble() - : point.GetSumDouble(); + ? point.GetGaugeLastValueDouble() + : point.GetSumDouble(); } return double.NaN; diff --git a/tests/SquidStd.Tests/Telemetry/OtlpProtocolTypeMappingTests.cs b/tests/SquidStd.Tests/Telemetry/OtlpProtocolTypeMappingTests.cs index 46293f45..fd757d99 100644 --- a/tests/SquidStd.Tests/Telemetry/OtlpProtocolTypeMappingTests.cs +++ b/tests/SquidStd.Tests/Telemetry/OtlpProtocolTypeMappingTests.cs @@ -8,13 +8,9 @@ public class OtlpProtocolTypeMappingTests { [Fact] public void Grpc_MapsToGrpc() - { - Assert.Equal(OtlpExportProtocol.Grpc, TelemetryPipeline.Map(OtlpProtocolType.Grpc)); - } + => Assert.Equal(OtlpExportProtocol.Grpc, TelemetryPipeline.Map(OtlpProtocolType.Grpc)); [Fact] public void HttpProtobuf_MapsToHttpProtobuf() - { - Assert.Equal(OtlpExportProtocol.HttpProtobuf, TelemetryPipeline.Map(OtlpProtocolType.HttpProtobuf)); - } + => Assert.Equal(OtlpExportProtocol.HttpProtobuf, TelemetryPipeline.Map(OtlpProtocolType.HttpProtobuf)); } diff --git a/tests/SquidStd.Tests/Telemetry/Support/FakeMetricsCollectionService.cs b/tests/SquidStd.Tests/Telemetry/Support/FakeMetricsCollectionService.cs index 837837a6..8c3d769a 100644 --- a/tests/SquidStd.Tests/Telemetry/Support/FakeMetricsCollectionService.cs +++ b/tests/SquidStd.Tests/Telemetry/Support/FakeMetricsCollectionService.cs @@ -10,21 +10,15 @@ public sealed class FakeMetricsCollectionService : IMetricsCollectionService public FakeMetricsCollectionService(IReadOnlyDictionary metrics) { - _snapshot = new MetricsSnapshot(DateTimeOffset.UnixEpoch, metrics); + _snapshot = new(DateTimeOffset.UnixEpoch, metrics); } public IReadOnlyDictionary GetAllMetrics() - { - return _snapshot.Metrics; - } + => _snapshot.Metrics; public MetricsSnapshot GetSnapshot() - { - return _snapshot; - } + => _snapshot; public MetricsSnapshot GetStatus() - { - return _snapshot; - } + => _snapshot; } diff --git a/tests/SquidStd.Tests/Telemetry/TelemetryRegistrationTests.cs b/tests/SquidStd.Tests/Telemetry/TelemetryRegistrationTests.cs index e91befc5..f0219a1c 100644 --- a/tests/SquidStd.Tests/Telemetry/TelemetryRegistrationTests.cs +++ b/tests/SquidStd.Tests/Telemetry/TelemetryRegistrationTests.cs @@ -5,7 +5,6 @@ using SquidStd.Abstractions.Interfaces.Services; using SquidStd.Core.Data.Metrics; using SquidStd.Core.Interfaces.Metrics; -using SquidStd.Telemetry.Abstractions.Data.Config; using SquidStd.Telemetry.OpenTelemetry.Extensions; using SquidStd.Telemetry.OpenTelemetry.Services; using SquidStd.Tests.Telemetry.Support; @@ -22,7 +21,7 @@ public async Task Container_RegistersTelemetryServiceAndStartsCleanly() new FakeMetricsCollectionService(new Dictionary()) ); - container.AddSquidStdTelemetry(new TelemetryOptions { EnableConsoleExporter = false }); + container.AddSquidStdTelemetry(new() { EnableConsoleExporter = false }); var service = container.Resolve(); Assert.IsAssignableFrom(service); @@ -39,7 +38,7 @@ public void ServiceCollection_RegistersTracerAndMeterProviders() new FakeMetricsCollectionService(new Dictionary()) ); - services.AddSquidStdTelemetry(new TelemetryOptions()); + services.AddSquidStdTelemetry(new()); using var provider = services.BuildServiceProvider(); diff --git a/tests/SquidStd.Tests/Telemetry/TracingTests.cs b/tests/SquidStd.Tests/Telemetry/TracingTests.cs index f95d2e7f..1bbfcab0 100644 --- a/tests/SquidStd.Tests/Telemetry/TracingTests.cs +++ b/tests/SquidStd.Tests/Telemetry/TracingTests.cs @@ -17,7 +17,7 @@ public void Pipeline_ExportsSpans() var exported = new List(); using var source = new ActivitySource(sourceName); - using (var provider = BuildProvider(new TelemetryOptions { ServiceName = "test-svc" }, sourceName, exported)) + using (var provider = BuildProvider(new() { ServiceName = "test-svc" }, sourceName, exported)) { using (var activity = source.StartActivity("do-work")) { @@ -38,11 +38,9 @@ public void Pipeline_WithZeroSampling_RecordsNothing() var exported = new List(); using var source = new ActivitySource(sourceName); - using (var provider = BuildProvider(new TelemetryOptions { TracingSampleRatio = 0.0 }, sourceName, exported)) + using (var provider = BuildProvider(new() { TracingSampleRatio = 0.0 }, sourceName, exported)) { - using (source.StartActivity("dropped")) - { - } + using (source.StartActivity("dropped")) { } provider.ForceFlush(); } diff --git a/tests/SquidStd.Tests/Templating/ScribanTemplateRendererTests.cs b/tests/SquidStd.Tests/Templating/ScribanTemplateRendererTests.cs index edf0eb31..be52fb88 100644 --- a/tests/SquidStd.Tests/Templating/ScribanTemplateRendererTests.cs +++ b/tests/SquidStd.Tests/Templating/ScribanTemplateRendererTests.cs @@ -1,4 +1,3 @@ -using SquidStd.Core.Directories; using SquidStd.Templating; using SquidStd.Templating.Services; using SquidStd.Tests.Support; @@ -83,7 +82,5 @@ public async Task StartAsync_AutoLoadsTemplatesFromDirectory() } private static ScribanTemplateRenderer NewRenderer(string root) - { - return new ScribanTemplateRenderer(new DirectoriesConfig(root, [])); - } + => new(new(root, [])); } diff --git a/tests/SquidStd.Tests/Tui/Binding/ViewBinderAutoBindTests.cs b/tests/SquidStd.Tests/Tui/Binding/ViewBinderAutoBindTests.cs index 220936cf..3e612338 100644 --- a/tests/SquidStd.Tests/Tui/Binding/ViewBinderAutoBindTests.cs +++ b/tests/SquidStd.Tests/Tui/Binding/ViewBinderAutoBindTests.cs @@ -8,31 +8,6 @@ namespace SquidStd.Tests.Tui.Binding; public partial class ViewBinderAutoBindTests { - private sealed partial class AutoBindViewModel : ObservableObject - { - [ObservableProperty] - private string _title = string.Empty; - - [ObservableProperty] - private string _name = string.Empty; - - // Wrong type on purpose — used by the "wrong type" skip-path test. - public int Count { get; } = 42; - - private bool _canSave; - - [RelayCommand(CanExecute = nameof(CanSave))] - private void Save() { } - - private bool CanSave() => _canSave; - - public void SetCanSave(bool value) - { - _canSave = value; - SaveCommand.NotifyCanExecuteChanged(); - } - } - // ------------------------------------------------------------------- // Label one-way // ------------------------------------------------------------------- @@ -161,4 +136,28 @@ public void AutoBind_WrongPropertyType_DoesNotThrow() Assert.Null(ex); Assert.Equal(string.Empty, label.Text); // left untouched } + + private sealed partial class AutoBindViewModel : ObservableObject + { + [ObservableProperty] private string _title = string.Empty; + + [ObservableProperty] private string _name = string.Empty; + + private bool _canSave; + + // Wrong type on purpose — used by the "wrong type" skip-path test. + public int Count { get; } = 42; + + [RelayCommand(CanExecute = nameof(CanSave))] + private void Save() { } + + private bool CanSave() + => _canSave; + + public void SetCanSave(bool value) + { + _canSave = value; + SaveCommand.NotifyCanExecuteChanged(); + } + } } diff --git a/tests/SquidStd.Tests/Tui/Binding/ViewBinderCommandTests.cs b/tests/SquidStd.Tests/Tui/Binding/ViewBinderCommandTests.cs index 14101e56..b3567f5c 100644 --- a/tests/SquidStd.Tests/Tui/Binding/ViewBinderCommandTests.cs +++ b/tests/SquidStd.Tests/Tui/Binding/ViewBinderCommandTests.cs @@ -10,6 +10,7 @@ public void Command_ExecutesTrigger_AndReflectsCanExecute() { var executed = 0; var canRun = false; + // ReSharper disable once AccessToModifiedClosure var command = new RelayCommand(() => executed++, () => canRun); var enabled = true; @@ -18,13 +19,13 @@ public void Command_ExecutesTrigger_AndReflectsCanExecute() binder.Command(command, isEnabled => enabled = isEnabled, cb => trigger = cb); - Assert.False(enabled); // initial CanExecute == false -> disabled + Assert.False(enabled); // initial CanExecute == false -> disabled canRun = true; command.NotifyCanExecuteChanged(); - Assert.True(enabled); // CanExecuteChanged -> enabled + Assert.True(enabled); // CanExecuteChanged -> enabled - trigger!(); // user activates the control + trigger!(); // user activates the control Assert.Equal(1, executed); } diff --git a/tests/SquidStd.Tests/Tui/Binding/ViewBinderOneWayTests.cs b/tests/SquidStd.Tests/Tui/Binding/ViewBinderOneWayTests.cs index 9a831f62..826b6df5 100644 --- a/tests/SquidStd.Tests/Tui/Binding/ViewBinderOneWayTests.cs +++ b/tests/SquidStd.Tests/Tui/Binding/ViewBinderOneWayTests.cs @@ -5,23 +5,6 @@ namespace SquidStd.Tests.Tui.Binding; public class ViewBinderOneWayTests { - private sealed class FakeViewModel : INotifyPropertyChanged - { - private string _title = string.Empty; - - public string Title - { - get { return _title; } - set - { - _title = value; - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Title))); - } - } - - public event PropertyChangedEventHandler? PropertyChanged; - } - [Fact] public void OneWay_AppliesInitialValueAndUpdates() { @@ -56,11 +39,34 @@ public void OneWay_UsesMarshal() { var vm = new FakeViewModel(); var marshalled = 0; - var binder = new ViewBinder(action => { marshalled++; action(); }); + var binder = new ViewBinder( + action => + { + marshalled++; + action(); + } + ); binder.OneWay(vm, nameof(FakeViewModel.Title), () => { }); vm.Title = "x"; Assert.Equal(1, marshalled); // change marshalled once; the initial apply is NOT marshalled } + + private sealed class FakeViewModel : INotifyPropertyChanged + { + private string _title = string.Empty; + + public string Title + { + get => _title; + set + { + _title = value; + PropertyChanged?.Invoke(this, new(nameof(Title))); + } + } + + public event PropertyChangedEventHandler? PropertyChanged; + } } diff --git a/tests/SquidStd.Tests/Tui/Binding/ViewBinderTwoWayTests.cs b/tests/SquidStd.Tests/Tui/Binding/ViewBinderTwoWayTests.cs index 586cff1d..fe354b62 100644 --- a/tests/SquidStd.Tests/Tui/Binding/ViewBinderTwoWayTests.cs +++ b/tests/SquidStd.Tests/Tui/Binding/ViewBinderTwoWayTests.cs @@ -5,47 +5,6 @@ namespace SquidStd.Tests.Tui.Binding; public class ViewBinderTwoWayTests { - private sealed class FakeViewModel : INotifyPropertyChanged - { - private string _name = string.Empty; - - public string Name - { - get { return _name; } - set - { - if (string.Equals(_name, value, StringComparison.Ordinal)) - { - return; - } - - _name = value; - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Name))); - } - } - - public event PropertyChangedEventHandler? PropertyChanged; - } - - private sealed class FakeField - { - private string _text = string.Empty; - - public string Text - { - get { return _text; } - set { _text = value; } - } - - public event Action? Changed; - - public void UserTypes(string value) - { - _text = value; - Changed?.Invoke(); - } - } - [Fact] public void TwoWay_PropagatesBothDirections_WithoutLooping() { @@ -54,10 +13,11 @@ public void TwoWay_PropagatesBothDirections_WithoutLooping() var binder = new ViewBinder(); binder.TwoWay( - vm, nameof(FakeViewModel.Name), - applyToTarget: () => field.Text = vm.Name, - subscribeTargetChanged: cb => field.Changed += () => cb(), - writeToSource: () => vm.Name = field.Text + vm, + nameof(FakeViewModel.Name), + () => field.Text = vm.Name, + cb => field.Changed += () => cb(), + () => vm.Name = field.Text ); Assert.Equal("init", field.Text); @@ -84,4 +44,39 @@ public void TwoWay_Typed_BindsViaExpressions() field.UserTypes("c"); Assert.Equal("c", vm.Name); } + + private sealed class FakeViewModel : INotifyPropertyChanged + { + private string _name = string.Empty; + + public string Name + { + get => _name; + set + { + if (string.Equals(_name, value, StringComparison.Ordinal)) + { + return; + } + + _name = value; + PropertyChanged?.Invoke(this, new(nameof(Name))); + } + } + + public event PropertyChangedEventHandler? PropertyChanged; + } + + private sealed class FakeField + { + public string Text { get; set; } = string.Empty; + + public void UserTypes(string value) + { + Text = value; + Changed?.Invoke(); + } + + public event Action? Changed; + } } diff --git a/tests/SquidStd.Tests/Tui/Dsl/TuiNodeMaterializerTests.cs b/tests/SquidStd.Tests/Tui/Dsl/TuiNodeMaterializerTests.cs index 72f367cc..bfa9fb41 100644 --- a/tests/SquidStd.Tests/Tui/Dsl/TuiNodeMaterializerTests.cs +++ b/tests/SquidStd.Tests/Tui/Dsl/TuiNodeMaterializerTests.cs @@ -7,15 +7,6 @@ namespace SquidStd.Tests.Tui.Dsl; public partial class TuiNodeMaterializerTests { - private sealed partial class SampleViewModel : ObservableObject - { - [ObservableProperty] - private string _title = "start"; - - [ObservableProperty] - private string _name = string.Empty; - } - [Fact] public void Materialize_VStack_ProducesChildrenAndAppliesOneWay() { @@ -46,4 +37,11 @@ public void Materialize_TextField_TwoWay_WritesBackToViewModel() vm.Name = "from-vm"; Assert.Equal("from-vm", field.Text); } + + private sealed partial class SampleViewModel : ObservableObject + { + [ObservableProperty] private string _title = "start"; + + [ObservableProperty] private string _name = string.Empty; + } } diff --git a/tests/SquidStd.Tests/Tui/Dsl/TuiNodeTests.cs b/tests/SquidStd.Tests/Tui/Dsl/TuiNodeTests.cs index 72ea2eba..135b3747 100644 --- a/tests/SquidStd.Tests/Tui/Dsl/TuiNodeTests.cs +++ b/tests/SquidStd.Tests/Tui/Dsl/TuiNodeTests.cs @@ -8,12 +8,6 @@ namespace SquidStd.Tests.Tui.Dsl; public class TuiNodeTests { - private sealed class SampleViewModel - { - public string Title { get; set; } = string.Empty; - public ICommand Save { get; } = new RelayCommand(() => { }); - } - [Fact] public void LabelNode_CapturesTextExpression() { @@ -50,4 +44,10 @@ public void StackNode_HoldsOrientationAndChildren() Assert.Single(node.Children); Assert.Same(child, node.Children[0]); } + + private sealed class SampleViewModel + { + public string Title { get; } = string.Empty; + public ICommand Save { get; } = new RelayCommand(() => { }); + } } diff --git a/tests/SquidStd.Tests/Tui/Dsl/UiFactoryTests.cs b/tests/SquidStd.Tests/Tui/Dsl/UiFactoryTests.cs index 7d897068..d4d11bb9 100644 --- a/tests/SquidStd.Tests/Tui/Dsl/UiFactoryTests.cs +++ b/tests/SquidStd.Tests/Tui/Dsl/UiFactoryTests.cs @@ -8,13 +8,6 @@ namespace SquidStd.Tests.Tui.Dsl; public class UiFactoryTests { - private sealed class SampleViewModel - { - public string Title { get; set; } = string.Empty; - public string Name { get; set; } = string.Empty; - public ICommand Save { get; } = new RelayCommand(() => { }); - } - private readonly UiFactory _ui = new(); [Fact] @@ -51,4 +44,11 @@ public void VStack_And_HStack_SetOrientationAndChildren() Assert.Equal(StackOrientation.Horizontal, h.Orientation); Assert.Single(h.Children); } + + private sealed class SampleViewModel + { + public string Title { get; } = string.Empty; + public string Name { get; } = string.Empty; + public ICommand Save { get; } = new RelayCommand(() => { }); + } } diff --git a/tests/SquidStd.Tests/Tui/Extensions/RegisterTuiExtensionsTests.cs b/tests/SquidStd.Tests/Tui/Extensions/RegisterTuiExtensionsTests.cs index 8aefa735..94cf84b6 100644 --- a/tests/SquidStd.Tests/Tui/Extensions/RegisterTuiExtensionsTests.cs +++ b/tests/SquidStd.Tests/Tui/Extensions/RegisterTuiExtensionsTests.cs @@ -9,37 +9,6 @@ namespace SquidStd.Tests.Tui.Extensions; public class RegisterTuiExtensionsTests { - private sealed class HomeViewModel : TuiViewModel - { - } - - private sealed class HomeView : ITuiView - { - public void Bind(object viewModel) - { - } - - public void Initialize() - { - } - } - - // Real views derive from Window (IDisposable); this fake reproduces that shape. - private sealed class DisposableView : ITuiView, IDisposable - { - public void Bind(object viewModel) - { - } - - public void Initialize() - { - } - - public void Dispose() - { - } - } - [Fact] public void RegisterTui_RegistersNavigatorAndRegistry() { @@ -78,4 +47,23 @@ public void RegisterView_AllowsDisposableTransientView() Assert.NotNull(container.Resolve()); } + + private sealed class HomeViewModel : TuiViewModel { } + + private sealed class HomeView : ITuiView + { + public void Bind(object viewModel) { } + + public void Initialize() { } + } + + // Real views derive from Window (IDisposable); this fake reproduces that shape. + private sealed class DisposableView : ITuiView, IDisposable + { + public void Bind(object viewModel) { } + + public void Initialize() { } + + public void Dispose() { } + } } diff --git a/tests/SquidStd.Tests/Tui/Internal/ConventionNamesTests.cs b/tests/SquidStd.Tests/Tui/Internal/ConventionNamesTests.cs index 3d23f76d..f92b031a 100644 --- a/tests/SquidStd.Tests/Tui/Internal/ConventionNamesTests.cs +++ b/tests/SquidStd.Tests/Tui/Internal/ConventionNamesTests.cs @@ -4,19 +4,12 @@ namespace SquidStd.Tests.Tui.Internal; public class ConventionNamesTests { - [Theory] - [InlineData("NameField", "Name")] - [InlineData("TitleLabel", "Title")] - [InlineData("SaveButton", "Save")] - [InlineData("Name", "Name")] + [Theory, InlineData("NameField", "Name"), InlineData("TitleLabel", "Title"), InlineData("SaveButton", "Save"), + InlineData("Name", "Name")] public void MemberName_StripsKnownWidgetSuffixes(string widgetId, string expected) - { - Assert.Equal(expected, ConventionNames.MemberName(widgetId)); - } + => Assert.Equal(expected, ConventionNames.MemberName(widgetId)); [Fact] public void CommandName_AppendsCommandSuffix() - { - Assert.Equal("SaveCommand", ConventionNames.CommandName("SaveButton")); - } + => Assert.Equal("SaveCommand", ConventionNames.CommandName("SaveButton")); } diff --git a/tests/SquidStd.Tests/Tui/Internal/PropertyPathTests.cs b/tests/SquidStd.Tests/Tui/Internal/PropertyPathTests.cs index d1411c99..452caf45 100644 --- a/tests/SquidStd.Tests/Tui/Internal/PropertyPathTests.cs +++ b/tests/SquidStd.Tests/Tui/Internal/PropertyPathTests.cs @@ -4,23 +4,13 @@ namespace SquidStd.Tests.Tui.Internal; public class PropertyPathTests { - private sealed class Sample - { - public string Name { get; set; } = string.Empty; - public int Count; - } - [Fact] public void NameOf_Property_ReturnsName() - { - Assert.Equal("Name", PropertyPath.NameOf(s => s.Name)); - } + => Assert.Equal("Name", PropertyPath.NameOf(s => s.Name)); [Fact] public void NameOf_Field_ReturnsName() - { - Assert.Equal("Count", PropertyPath.NameOf(s => s.Count)); - } + => Assert.Equal("Count", PropertyPath.NameOf(s => s.Count)); [Fact] public void Setter_Property_WritesValue() @@ -35,7 +25,11 @@ public void Setter_Property_WritesValue() [Fact] public void NameOf_NonMemberExpression_Throws() + => Assert.Throws(() => PropertyPath.NameOf(s => s.Count + 1)); + + private sealed class Sample { - Assert.Throws(() => PropertyPath.NameOf(s => s.Count + 1)); + public int Count; + public string Name { get; set; } = string.Empty; } } diff --git a/tests/SquidStd.Tests/Tui/Navigation/TuiNavigatorTests.cs b/tests/SquidStd.Tests/Tui/Navigation/TuiNavigatorTests.cs index aeb072f9..6f117c5d 100644 --- a/tests/SquidStd.Tests/Tui/Navigation/TuiNavigatorTests.cs +++ b/tests/SquidStd.Tests/Tui/Navigation/TuiNavigatorTests.cs @@ -8,47 +8,6 @@ namespace SquidStd.Tests.Tui.Navigation; public class TuiNavigatorTests { - private sealed class HomeViewModel : TuiViewModel - { - } - - private sealed class DetailViewModel : TuiViewModel - { - } - - // Non-generic fake view that records the ViewModel it was given and that it was initialised. - private sealed class FakeView : ITuiView - { - public object? BoundViewModel { get; private set; } - public bool Initialized { get; private set; } - - public void Bind(object viewModel) - { - BoundViewModel = viewModel; - } - - public void Initialize() - { - Initialized = true; - } - } - - private sealed class FakeViewHost : ITuiViewHost - { - public List Shown { get; } = new(); - public List Removed { get; } = new(); - - public void Show(object view) - { - Shown.Add(view); - } - - public void Remove(object view) - { - Removed.Add(view); - } - } - private static (TuiNavigator Navigator, FakeViewHost Host) Build() { var container = new Container(); @@ -107,6 +66,66 @@ public async Task Back_OnLastScreen_DoesNothing() Assert.Equal(1, navigator.Depth); // refuses to pop the root } + [Fact] + public async Task ForwardThenBack_PairsActivationHooks_NoDoubleActivateWithoutDeactivate() + { + var container = new Container(); + container.Register(Reuse.Singleton); + container.Register(Reuse.Singleton); + container.Register(Reuse.Transient); + + var registry = new TuiViewRegistry(); + registry.Map(typeof(TrackingHomeViewModel), typeof(FakeView)); + registry.Map(typeof(TrackingDetailViewModel), typeof(FakeView)); + + var navigator = new TuiNavigator(container, registry, new FakeViewHost()); + var home = container.Resolve(); + var detail = container.Resolve(); + + await navigator.NavigateToAsync(); // home: A1 D0 + Assert.Equal(1, home.Activated); + Assert.Equal(0, home.Deactivated); + + await navigator.NavigateToAsync(); // home deactivated; detail A1 + Assert.Equal(1, home.Activated); + Assert.Equal(1, home.Deactivated); + Assert.Equal(1, detail.Activated); + + await navigator.BackAsync(); // detail D1; home reactivated + Assert.Equal(1, detail.Deactivated); + Assert.Equal(2, home.Activated); + Assert.Equal(1, home.Deactivated); // still 1 — every activate is now paired with a prior deactivate + } + + private sealed class HomeViewModel : TuiViewModel { } + + private sealed class DetailViewModel : TuiViewModel { } + + // Non-generic fake view that records the ViewModel it was given and that it was initialised. + private sealed class FakeView : ITuiView + { + public object? BoundViewModel { get; private set; } + public bool Initialized { get; private set; } + + public void Bind(object viewModel) + => BoundViewModel = viewModel; + + public void Initialize() + => Initialized = true; + } + + private sealed class FakeViewHost : ITuiViewHost + { + public List Shown { get; } = new(); + public List Removed { get; } = new(); + + public void Show(object view) + => Shown.Add(view); + + public void Remove(object view) + => Removed.Add(view); + } + private sealed class TrackingHomeViewModel : TuiViewModel { public int Activated { get; private set; } @@ -115,12 +134,14 @@ private sealed class TrackingHomeViewModel : TuiViewModel public override ValueTask OnActivatedAsync() { Activated++; + return ValueTask.CompletedTask; } public override ValueTask OnDeactivatedAsync() { Deactivated++; + return ValueTask.CompletedTask; } } @@ -133,44 +154,15 @@ private sealed class TrackingDetailViewModel : TuiViewModel public override ValueTask OnActivatedAsync() { Activated++; + return ValueTask.CompletedTask; } public override ValueTask OnDeactivatedAsync() { Deactivated++; + return ValueTask.CompletedTask; } } - - [Fact] - public async Task ForwardThenBack_PairsActivationHooks_NoDoubleActivateWithoutDeactivate() - { - var container = new Container(); - container.Register(Reuse.Singleton); - container.Register(Reuse.Singleton); - container.Register(Reuse.Transient); - - var registry = new TuiViewRegistry(); - registry.Map(typeof(TrackingHomeViewModel), typeof(FakeView)); - registry.Map(typeof(TrackingDetailViewModel), typeof(FakeView)); - - var navigator = new TuiNavigator(container, registry, new FakeViewHost()); - var home = container.Resolve(); - var detail = container.Resolve(); - - await navigator.NavigateToAsync(); // home: A1 D0 - Assert.Equal(1, home.Activated); - Assert.Equal(0, home.Deactivated); - - await navigator.NavigateToAsync(); // home deactivated; detail A1 - Assert.Equal(1, home.Activated); - Assert.Equal(1, home.Deactivated); - Assert.Equal(1, detail.Activated); - - await navigator.BackAsync(); // detail D1; home reactivated - Assert.Equal(1, detail.Deactivated); - Assert.Equal(2, home.Activated); - Assert.Equal(1, home.Deactivated); // still 1 — every activate is now paired with a prior deactivate - } } diff --git a/tests/SquidStd.Tests/Tui/TuiViewModelTests.cs b/tests/SquidStd.Tests/Tui/TuiViewModelTests.cs index 6cd048e3..28427ec9 100644 --- a/tests/SquidStd.Tests/Tui/TuiViewModelTests.cs +++ b/tests/SquidStd.Tests/Tui/TuiViewModelTests.cs @@ -5,21 +5,6 @@ namespace SquidStd.Tests.Tui; public partial class TuiViewModelTests { - private sealed partial class SampleViewModel : TuiViewModel - { - [ObservableProperty] - private string _title = string.Empty; - - public int Activated { get; private set; } - - public override ValueTask OnActivatedAsync() - { - Activated++; - - return ValueTask.CompletedTask; - } - } - [Fact] public void ObservableProperty_RaisesPropertyChanged() { @@ -49,4 +34,18 @@ public async Task OnDeactivatedAsync_DefaultsToNoOp() await vm.OnDeactivatedAsync(); // does not throw } + + private sealed partial class SampleViewModel : TuiViewModel + { + [ObservableProperty] private string _title = string.Empty; + + public int Activated { get; private set; } + + public override ValueTask OnActivatedAsync() + { + Activated++; + + return ValueTask.CompletedTask; + } + } } diff --git a/tests/SquidStd.Tests/Utils/DirectoriesUtilsTests.cs b/tests/SquidStd.Tests/Utils/DirectoriesUtilsTests.cs index 9f30bddd..2a7c61a9 100644 --- a/tests/SquidStd.Tests/Utils/DirectoriesUtilsTests.cs +++ b/tests/SquidStd.Tests/Utils/DirectoriesUtilsTests.cs @@ -33,9 +33,7 @@ public void GetFiles_NoExtensionFilter_ReturnsAllFiles() [Fact] public void GetFiles_NonExistentDirectory_ReturnsEmpty() - { - Assert.Empty(DirectoriesUtils.GetFiles(Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")))); - } + => Assert.Empty(DirectoriesUtils.GetFiles(Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")))); [Fact] public void GetFiles_NonRecursive_ExcludesNestedFiles() diff --git a/tests/SquidStd.Tests/Utils/HashUtilsTests.cs b/tests/SquidStd.Tests/Utils/HashUtilsTests.cs index d97a393a..be8ca6dc 100644 --- a/tests/SquidStd.Tests/Utils/HashUtilsTests.cs +++ b/tests/SquidStd.Tests/Utils/HashUtilsTests.cs @@ -4,14 +4,9 @@ namespace SquidStd.Tests.Utils; public class HashUtilsTests { - [Theory] - [InlineData("")] - [InlineData(" ")] - [InlineData(null)] + [Theory, InlineData(""), InlineData(" "), InlineData(null)] public void HashPassword_NullOrWhitespace_Throws(string? password) - { - Assert.Throws(() => HashUtils.HashPassword(password!)); - } + => Assert.Throws(() => HashUtils.HashPassword(password!)); [Fact] public void HashPassword_SamePasswordTwice_ProducesDifferentHashes() @@ -44,25 +39,15 @@ public void VerifyPassword_CorrectPassword_ReturnsTrue() Assert.True(HashUtils.VerifyPassword("s3cret", hash)); } - [Theory] - [InlineData("not-a-hash")] - [InlineData("md5$100000$c2FsdA==$aGFzaA==")] - [InlineData("pbkdf2-sha256$abc$c2FsdA==$aGFzaA==")] - [InlineData("pbkdf2-sha256$0$c2FsdA==$aGFzaA==")] - [InlineData("pbkdf2-sha256$100000$not-base64$aGFzaA==")] + [Theory, InlineData("not-a-hash"), InlineData("md5$100000$c2FsdA==$aGFzaA=="), + InlineData("pbkdf2-sha256$abc$c2FsdA==$aGFzaA=="), InlineData("pbkdf2-sha256$0$c2FsdA==$aGFzaA=="), + InlineData("pbkdf2-sha256$100000$not-base64$aGFzaA==")] public void VerifyPassword_MalformedStoredHash_ReturnsFalse(string storedHash) - { - Assert.False(HashUtils.VerifyPassword("s3cret", storedHash)); - } + => Assert.False(HashUtils.VerifyPassword("s3cret", storedHash)); - [Theory] - [InlineData("")] - [InlineData(" ")] - [InlineData(null)] + [Theory, InlineData(""), InlineData(" "), InlineData(null)] public void VerifyPassword_NullOrWhitespacePassword_ReturnsFalse(string? password) - { - Assert.False(HashUtils.VerifyPassword(password!, "pbkdf2-sha256$100000$c2FsdA==$aGFzaA==")); - } + => Assert.False(HashUtils.VerifyPassword(password!, "pbkdf2-sha256$100000$c2FsdA==$aGFzaA==")); [Fact] public void VerifyPassword_WrongPassword_ReturnsFalse() diff --git a/tests/SquidStd.Tests/Utils/NetworkUtilsTests.cs b/tests/SquidStd.Tests/Utils/NetworkUtilsTests.cs index 1c1a2a87..6d634a6c 100644 --- a/tests/SquidStd.Tests/Utils/NetworkUtilsTests.cs +++ b/tests/SquidStd.Tests/Utils/NetworkUtilsTests.cs @@ -7,9 +7,7 @@ public class NetworkUtilsTests { [Fact] public void GetListeningAddresses_NullEndpoint_Throws() - { - Assert.Throws(() => NetworkUtils.GetListeningAddresses(null!).ToList()); - } + => Assert.Throws(() => NetworkUtils.GetListeningAddresses(null!).ToList()); [Fact] public void GetListeningAddresses_ReturnsEndpointsMatchingFamilyAndPort() @@ -28,72 +26,43 @@ public void GetListeningAddresses_ReturnsEndpointsMatchingFamilyAndPort() ); } - [Theory] - [InlineData("")] - [InlineData(" ")] + [Theory, InlineData(""), InlineData(" ")] public void ParseIpAddress_NullOrWhitespace_Throws(string ipAddress) - { - Assert.Throws(() => NetworkUtils.ParseIpAddress(ipAddress)); - } + => Assert.Throws(() => NetworkUtils.ParseIpAddress(ipAddress)); [Fact] public void ParseIpAddress_ValidAddress_ReturnsParsedAddress() - { - Assert.Equal(IPAddress.Parse("127.0.0.1"), NetworkUtils.ParseIpAddress("127.0.0.1")); - } + => Assert.Equal(IPAddress.Parse("127.0.0.1"), NetworkUtils.ParseIpAddress("127.0.0.1")); [Fact] public void ParseIpAddress_Wildcard_ReturnsAny() - { - Assert.Equal(IPAddress.Any, NetworkUtils.ParseIpAddress("*")); - } + => Assert.Equal(IPAddress.Any, NetworkUtils.ParseIpAddress("*")); - [Theory] - [InlineData("99999")] - [InlineData("-1")] - [InlineData("abc")] + [Theory, InlineData("99999"), InlineData("-1"), InlineData("abc")] public void ParsePorts_InvalidPort_ThrowsFormatException(string ports) - { - Assert.Throws(() => NetworkUtils.ParsePorts(ports)); - } + => Assert.Throws(() => NetworkUtils.ParsePorts(ports)); - [Theory] - [InlineData("8000-7000")] - [InlineData("1-2-3")] + [Theory, InlineData("8000-7000"), InlineData("1-2-3")] public void ParsePorts_InvalidRange_ThrowsFormatException(string ports) - { - Assert.Throws(() => NetworkUtils.ParsePorts(ports)); - } + => Assert.Throws(() => NetworkUtils.ParsePorts(ports)); [Fact] public void ParsePorts_MixedRangeAndList_ReturnsAllPorts() - { - Assert.Equal([6666, 6667, 6668, 6669, 8000], NetworkUtils.ParsePorts("6666-6668,6669,8000")); - } + => Assert.Equal([6666, 6667, 6668, 6669, 8000], NetworkUtils.ParsePorts("6666-6668,6669,8000")); - [Theory] - [InlineData("")] - [InlineData(" ")] + [Theory, InlineData(""), InlineData(" ")] public void ParsePorts_NullOrWhitespace_ThrowsArgumentException(string ports) - { - Assert.Throws(() => NetworkUtils.ParsePorts(ports)); - } + => Assert.Throws(() => NetworkUtils.ParsePorts(ports)); [Fact] public void ParsePorts_Range_ReturnsExpandedRange() - { - Assert.Equal([6666, 6667, 6668], NetworkUtils.ParsePorts("6666-6668")); - } + => Assert.Equal([6666, 6667, 6668], NetworkUtils.ParsePorts("6666-6668")); [Fact] public void ParsePorts_SinglePort_ReturnsSingleEntry() - { - Assert.Equal([8000], NetworkUtils.ParsePorts("8000")); - } + => Assert.Equal([8000], NetworkUtils.ParsePorts("8000")); [Fact] public void ParsePorts_TrimsWhitespaceEntries() - { - Assert.Equal([80, 443], NetworkUtils.ParsePorts(" 80 , 443 ")); - } + => Assert.Equal([80, 443], NetworkUtils.ParsePorts(" 80 , 443 ")); } diff --git a/tests/SquidStd.Tests/Utils/PlatformUtilsTests.cs b/tests/SquidStd.Tests/Utils/PlatformUtilsTests.cs index 874e6bab..01e1512e 100644 --- a/tests/SquidStd.Tests/Utils/PlatformUtilsTests.cs +++ b/tests/SquidStd.Tests/Utils/PlatformUtilsTests.cs @@ -9,27 +9,21 @@ public class PlatformUtilsTests public void GetCurrentPlatform_MatchesOperatingSystemDetection() { var expected = OperatingSystem.IsWindows() ? PlatformType.Windows : - OperatingSystem.IsMacOS() ? PlatformType.MacOS : - OperatingSystem.IsLinux() ? PlatformType.Linux : PlatformType.Unknown; + OperatingSystem.IsMacOS() ? PlatformType.MacOS : + OperatingSystem.IsLinux() ? PlatformType.Linux : PlatformType.Unknown; Assert.Equal(expected, PlatformUtils.GetCurrentPlatform()); } [Fact] public void IsRunningOnLinux_MatchesOperatingSystem() - { - Assert.Equal(OperatingSystem.IsLinux(), PlatformUtils.IsRunningOnLinux()); - } + => Assert.Equal(OperatingSystem.IsLinux(), PlatformUtils.IsRunningOnLinux()); [Fact] public void IsRunningOnMacOS_MatchesOperatingSystem() - { - Assert.Equal(OperatingSystem.IsMacOS(), PlatformUtils.IsRunningOnMacOS()); - } + => Assert.Equal(OperatingSystem.IsMacOS(), PlatformUtils.IsRunningOnMacOS()); [Fact] public void IsRunningOnWindows_MatchesOperatingSystem() - { - Assert.Equal(OperatingSystem.IsWindows(), PlatformUtils.IsRunningOnWindows()); - } + => Assert.Equal(OperatingSystem.IsWindows(), PlatformUtils.IsRunningOnWindows()); } diff --git a/tests/SquidStd.Tests/Utils/ResourceUtilsTests.cs b/tests/SquidStd.Tests/Utils/ResourceUtilsTests.cs index 9b23e390..641f8c32 100644 --- a/tests/SquidStd.Tests/Utils/ResourceUtilsTests.cs +++ b/tests/SquidStd.Tests/Utils/ResourceUtilsTests.cs @@ -18,9 +18,7 @@ public void ConvertResourceNameToPath_NestedName_ConvertsDotsToSeparators() [Fact] public void ConvertResourceNameToPath_NoExtension_Throws() - { - Assert.Throws(() => ResourceUtils.ConvertResourceNameToPath("Asm.file", "Asm")); - } + => Assert.Throws(() => ResourceUtils.ConvertResourceNameToPath("Asm.file", "Asm")); [Fact] public void ConvertResourceNameToPath_ValidName_ReturnsFilePath() @@ -32,15 +30,11 @@ public void ConvertResourceNameToPath_ValidName_ReturnsFilePath() [Fact] public void ConvertResourceNameToPath_WrongNamespace_Throws() - { - Assert.Throws(() => ResourceUtils.ConvertResourceNameToPath("Other.cfg.json", "Asm")); - } + => Assert.Throws(() => ResourceUtils.ConvertResourceNameToPath("Other.cfg.json", "Asm")); [Fact] public void EmbeddedNameToPath_StripsPrefixAndConvertsDots() - { - Assert.Equal("Folder/file/txt", ResourceUtils.EmbeddedNameToPath("Asm.Folder.file.txt", "Asm")); - } + => Assert.Equal("Folder/file/txt", ResourceUtils.EmbeddedNameToPath("Asm.Folder.file.txt", "Asm")); [Fact] public void GetDirectoryPathFromResourceName_RemovesBaseNamespace() @@ -68,11 +62,10 @@ public void GetEmbeddedResourceNames_NoFilter_IncludesSampleResource() [Fact] public void GetEmbeddedResourceStream_MissingResource_Throws() - { - Assert.Throws(() => - ResourceUtils.GetEmbeddedResourceStream(TestAssembly, "does-not-exist.bin") + => Assert.Throws( + () => + ResourceUtils.GetEmbeddedResourceStream(TestAssembly, "does-not-exist.bin") ); - } [Fact] public void GetEmbeddedResourceString_ExistingResource_ReturnsContent() @@ -84,18 +77,11 @@ public void GetEmbeddedResourceString_ExistingResource_ReturnsContent() [Fact] public void GetFileNameFromResourceName_ReturnsFileNameWithExtension() - { - Assert.Equal("DefaultUiFont.ttf", ResourceUtils.GetFileNameFromResourceName("Assets.Fonts.DefaultUiFont.ttf")); - } + => Assert.Equal("DefaultUiFont.ttf", ResourceUtils.GetFileNameFromResourceName("Assets.Fonts.DefaultUiFont.ttf")); - [Theory] - [InlineData("a/b/c.txt", "c.txt")] - [InlineData("a\\b\\c.txt", "c.txt")] - [InlineData("c.txt", "c.txt")] + [Theory, InlineData("a/b/c.txt", "c.txt"), InlineData("a\\b\\c.txt", "c.txt"), InlineData("c.txt", "c.txt")] public void GetFileNameFromResourcePath_ReturnsFinalSegment(string input, string expected) - { - Assert.Equal(expected, ResourceUtils.GetFileNameFromResourcePath(input)); - } + => Assert.Equal(expected, ResourceUtils.GetFileNameFromResourcePath(input)); [Fact] public void ReadEmbeddedResource_ExistingResource_ReturnsContent() @@ -108,8 +94,7 @@ public void ReadEmbeddedResource_ExistingResource_ReturnsContent() [Fact] public void ReadEmbeddedResource_MissingResource_Throws() - { - Assert.Throws(() => ResourceUtils.ReadEmbeddedResource("does-not-exist.bin", TestAssembly) + => Assert.Throws( + () => ResourceUtils.ReadEmbeddedResource("does-not-exist.bin", TestAssembly) ); - } } diff --git a/tests/SquidStd.Tests/Utils/StringUtilsTests.cs b/tests/SquidStd.Tests/Utils/StringUtilsTests.cs index 950798f8..c2850852 100644 --- a/tests/SquidStd.Tests/Utils/StringUtilsTests.cs +++ b/tests/SquidStd.Tests/Utils/StringUtilsTests.cs @@ -4,118 +4,65 @@ namespace SquidStd.Tests.Utils; public class StringUtilsTests { - [Theory] - [InlineData("")] - [InlineData(null)] + [Theory, InlineData(""), InlineData(null)] public void ToCamelCase_NullOrEmpty_ReturnsEmpty(string? input) - { - Assert.Equal("", StringUtils.ToCamelCase(input!)); - } + => Assert.Equal("", StringUtils.ToCamelCase(input!)); [Fact] public void ToCamelCase_SingleCharacter_ReturnsLowerCase() - { - Assert.Equal("a", StringUtils.ToCamelCase("A")); - } + => Assert.Equal("a", StringUtils.ToCamelCase("A")); - [Theory] - [InlineData("HelloWorld", "helloWorld")] - [InlineData("API_RESPONSE", "apiResponse")] - [InlineData("user-id", "userId")] - [InlineData("hello world", "helloWorld")] + [Theory, InlineData("HelloWorld", "helloWorld"), InlineData("API_RESPONSE", "apiResponse"), + InlineData("user-id", "userId"), InlineData("hello world", "helloWorld")] public void ToCamelCase_VariousInputs_ReturnsCamelCase(string input, string expected) - { - Assert.Equal(expected, StringUtils.ToCamelCase(input)); - } + => Assert.Equal(expected, StringUtils.ToCamelCase(input)); - [Theory] - [InlineData("HelloWorld", "hello.world")] - [InlineData("API_RESPONSE", "api.response")] + [Theory, InlineData("HelloWorld", "hello.world"), InlineData("API_RESPONSE", "api.response")] public void ToDotCase_VariousInputs_ReturnsDotCase(string input, string expected) - { - Assert.Equal(expected, StringUtils.ToDotCase(input)); - } + => Assert.Equal(expected, StringUtils.ToDotCase(input)); - [Theory] - [InlineData("HelloWorld", "hello-world")] - [InlineData("API_RESPONSE", "api-response")] - [InlineData("userId", "user-id")] + [Theory, InlineData("HelloWorld", "hello-world"), InlineData("API_RESPONSE", "api-response"), + InlineData("userId", "user-id")] public void ToKebabCase_VariousInputs_ReturnsKebabCase(string input, string expected) - { - Assert.Equal(expected, StringUtils.ToKebabCase(input)); - } + => Assert.Equal(expected, StringUtils.ToKebabCase(input)); [Fact] public void ToPascalCase_SingleCharacter_ReturnsUpperCase() - { - Assert.Equal("A", StringUtils.ToPascalCase("a")); - } + => Assert.Equal("A", StringUtils.ToPascalCase("a")); - [Theory] - [InlineData("hello_world", "HelloWorld")] - [InlineData("api-response", "ApiResponse")] - [InlineData("userId", "UserId")] + [Theory, InlineData("hello_world", "HelloWorld"), InlineData("api-response", "ApiResponse"), + InlineData("userId", "UserId")] public void ToPascalCase_VariousInputs_ReturnsPascalCase(string input, string expected) - { - Assert.Equal(expected, StringUtils.ToPascalCase(input)); - } + => Assert.Equal(expected, StringUtils.ToPascalCase(input)); - [Theory] - [InlineData("HelloWorld", "hello/world")] - [InlineData("API_RESPONSE", "api/response")] + [Theory, InlineData("HelloWorld", "hello/world"), InlineData("API_RESPONSE", "api/response")] public void ToPathCase_VariousInputs_ReturnsPathCase(string input, string expected) - { - Assert.Equal(expected, StringUtils.ToPathCase(input)); - } + => Assert.Equal(expected, StringUtils.ToPathCase(input)); - [Theory] - [InlineData("hello world", "Hello world")] - [InlineData("API_RESPONSE", "Api response")] + [Theory, InlineData("hello world", "Hello world"), InlineData("API_RESPONSE", "Api response")] public void ToSentenceCase_VariousInputs_ReturnsSentenceCase(string input, string expected) - { - Assert.Equal(expected, StringUtils.ToSentenceCase(input)); - } + => Assert.Equal(expected, StringUtils.ToSentenceCase(input)); - [Theory] - [InlineData("")] - [InlineData(null)] + [Theory, InlineData(""), InlineData(null)] public void ToSnakeCase_NullOrEmpty_ReturnsEmpty(string? input) - { - Assert.Equal("", StringUtils.ToSnakeCase(input!)); - } + => Assert.Equal("", StringUtils.ToSnakeCase(input!)); - [Theory] - [InlineData("HelloWorld", "hello_world")] - [InlineData("APIResponse", "api_response")] - [InlineData("userId", "user_id")] + [Theory, InlineData("HelloWorld", "hello_world"), InlineData("APIResponse", "api_response"), + InlineData("userId", "user_id")] public void ToSnakeCase_VariousInputs_ReturnsSnakeCase(string input, string expected) - { - Assert.Equal(expected, StringUtils.ToSnakeCase(input)); - } + => Assert.Equal(expected, StringUtils.ToSnakeCase(input)); - [Theory] - [InlineData("hello_world", "Hello World")] - [InlineData("API_RESPONSE", "Api Response")] - [InlineData("user-id", "User Id")] + [Theory, InlineData("hello_world", "Hello World"), InlineData("API_RESPONSE", "Api Response"), + InlineData("user-id", "User Id")] public void ToTitleCase_VariousInputs_ReturnsTitleCase(string input, string expected) - { - Assert.Equal(expected, StringUtils.ToTitleCase(input)); - } + => Assert.Equal(expected, StringUtils.ToTitleCase(input)); - [Theory] - [InlineData("hello_world", "Hello-World")] - [InlineData("apiResponse", "Api-Response")] + [Theory, InlineData("hello_world", "Hello-World"), InlineData("apiResponse", "Api-Response")] public void ToTrainCase_VariousInputs_ReturnsTrainCase(string input, string expected) - { - Assert.Equal(expected, StringUtils.ToTrainCase(input)); - } + => Assert.Equal(expected, StringUtils.ToTrainCase(input)); - [Theory] - [InlineData("HelloWorld", "HELLO_WORLD")] - [InlineData("apiResponse", "API_RESPONSE")] - [InlineData("user-id", "USER_ID")] + [Theory, InlineData("HelloWorld", "HELLO_WORLD"), InlineData("apiResponse", "API_RESPONSE"), + InlineData("user-id", "USER_ID")] public void ToUpperSnakeCase_VariousInputs_ReturnsScreamingSnakeCase(string input, string expected) - { - Assert.Equal(expected, StringUtils.ToUpperSnakeCase(input)); - } + => Assert.Equal(expected, StringUtils.ToUpperSnakeCase(input)); } diff --git a/tests/SquidStd.Tests/Utils/VersionUtilsTests.cs b/tests/SquidStd.Tests/Utils/VersionUtilsTests.cs index aff8ef90..5347ceb4 100644 --- a/tests/SquidStd.Tests/Utils/VersionUtilsTests.cs +++ b/tests/SquidStd.Tests/Utils/VersionUtilsTests.cs @@ -24,7 +24,5 @@ public void GetVersion_ExplicitAssembly_ReturnsNonEmptyVersion() [Fact] public void GetVersion_NullAssembly_Throws() - { - Assert.Throws(() => VersionUtils.GetVersion(null!)); - } + => Assert.Throws(() => VersionUtils.GetVersion(null!)); } diff --git a/tests/SquidStd.Tests/Vfs/InMemoryFileSystemTests.cs b/tests/SquidStd.Tests/Vfs/InMemoryFileSystemTests.cs index 8a2847b0..08fa9e4a 100644 --- a/tests/SquidStd.Tests/Vfs/InMemoryFileSystemTests.cs +++ b/tests/SquidStd.Tests/Vfs/InMemoryFileSystemTests.cs @@ -15,6 +15,7 @@ public async Task Write_Read_List_Delete_RoundTrips() Assert.Equal("x", Encoding.UTF8.GetString((await fs.ReadAllBytesAsync("a/b.txt"))!)); var paths = new List(); + await foreach (var e in fs.ListAsync("a")) { paths.Add(e.Path); diff --git a/tests/SquidStd.Tests/Vfs/PhysicalFileSystemTests.cs b/tests/SquidStd.Tests/Vfs/PhysicalFileSystemTests.cs index f3efa01e..cee9224a 100644 --- a/tests/SquidStd.Tests/Vfs/PhysicalFileSystemTests.cs +++ b/tests/SquidStd.Tests/Vfs/PhysicalFileSystemTests.cs @@ -19,6 +19,7 @@ public async Task Write_Read_List_Delete_RoundTrips() Assert.Equal("hello", Encoding.UTF8.GetString((await fs.ReadAllBytesAsync("docs/cv.pdf"))!)); var entries = new List(); + await foreach (var e in fs.ListAsync()) { entries.Add(e.Path); @@ -35,7 +36,7 @@ public async Task Write_Read_List_Delete_RoundTrips() { if (Directory.Exists(root)) { - Directory.Delete(root, recursive: true); + Directory.Delete(root, true); } } } diff --git a/tests/SquidStd.Tests/Vfs/VfsPathTests.cs b/tests/SquidStd.Tests/Vfs/VfsPathTests.cs index 903666fd..35fb995e 100644 --- a/tests/SquidStd.Tests/Vfs/VfsPathTests.cs +++ b/tests/SquidStd.Tests/Vfs/VfsPathTests.cs @@ -4,20 +4,12 @@ namespace SquidStd.Tests.Vfs; public class VfsPathTests { - [Theory] - [InlineData("docs/cv.pdf", "docs/cv.pdf")] - [InlineData("/docs//cv.pdf", "docs/cv.pdf")] - [InlineData("docs\\cv.pdf", "docs/cv.pdf")] + [Theory, InlineData("docs/cv.pdf", "docs/cv.pdf"), InlineData("/docs//cv.pdf", "docs/cv.pdf"), + InlineData("docs\\cv.pdf", "docs/cv.pdf")] public void Normalize_ProducesForwardSlashRelativePath(string input, string expected) - { - Assert.Equal(expected, VfsPath.Normalize(input)); - } + => Assert.Equal(expected, VfsPath.Normalize(input)); - [Theory] - [InlineData("../escape")] - [InlineData("a/../../b")] + [Theory, InlineData("../escape"), InlineData("a/../../b")] public void Normalize_RejectsTraversal(string input) - { - Assert.Throws(() => VfsPath.Normalize(input)); - } + => Assert.Throws(() => VfsPath.Normalize(input)); } diff --git a/tests/SquidStd.Tests/Workers/JobDispatcherTests.cs b/tests/SquidStd.Tests/Workers/JobDispatcherTests.cs index 108dd995..d5b7b6c1 100644 --- a/tests/SquidStd.Tests/Workers/JobDispatcherTests.cs +++ b/tests/SquidStd.Tests/Workers/JobDispatcherTests.cs @@ -26,7 +26,8 @@ public async Task DispatchAsync_PropagatesHandlerException() var boom = new RecordingJobHandler("resize") { ThrowOnHandle = new InvalidOperationException("boom") }; var dispatcher = new JobDispatcher([boom]); - await Assert.ThrowsAsync(() => dispatcher.DispatchAsync( + await Assert.ThrowsAsync( + () => dispatcher.DispatchAsync( Job("resize"), CancellationToken.None ) @@ -38,15 +39,14 @@ public async Task DispatchAsync_ThrowsWhenNoHandlerMatches() { var dispatcher = new JobDispatcher([new RecordingJobHandler("resize")]); - var ex = await Assert.ThrowsAsync(() => - dispatcher.DispatchAsync(Job("unknown"), CancellationToken.None) - ); + var ex = await Assert.ThrowsAsync( + () => + dispatcher.DispatchAsync(Job("unknown"), CancellationToken.None) + ); Assert.Equal("unknown", ex.JobName); } private static JobRequest Job(string name) - { - return new JobRequest(name, new Dictionary()); - } + => new(name, new Dictionary()); } diff --git a/tests/SquidStd.Tests/Workers/Support/FakeMessageQueue.cs b/tests/SquidStd.Tests/Workers/Support/FakeMessageQueue.cs index 35d72091..3e961992 100644 --- a/tests/SquidStd.Tests/Workers/Support/FakeMessageQueue.cs +++ b/tests/SquidStd.Tests/Workers/Support/FakeMessageQueue.cs @@ -6,24 +6,16 @@ namespace SquidStd.Tests.Workers.Support; public sealed class FakeMessageQueue : IMessageQueue { public Task PublishAsync(string queueName, TMessage message, CancellationToken cancellationToken = default) - { - return Task.CompletedTask; - } + => Task.CompletedTask; public IDisposable Subscribe(string queueName, IQueueMessageListener listener) - { - return new Subscription(); - } + => new Subscription(); public IDisposable Subscribe(string queueName, IQueueMessageListenerAsync listener) - { - return new Subscription(); - } + => new Subscription(); private sealed class Subscription : IDisposable { - public void Dispose() - { - } + public void Dispose() { } } } diff --git a/tests/SquidStd.Tests/Workers/Support/RecordingJobHandler.cs b/tests/SquidStd.Tests/Workers/Support/RecordingJobHandler.cs index d94aae33..e16a7fd1 100644 --- a/tests/SquidStd.Tests/Workers/Support/RecordingJobHandler.cs +++ b/tests/SquidStd.Tests/Workers/Support/RecordingJobHandler.cs @@ -4,8 +4,8 @@ namespace SquidStd.Tests.Workers.Support; /// -/// Configurable for tests: records the jobs it received and can be told -/// to throw, or to block until released, so concurrency and error paths can be exercised. +/// Configurable for tests: records the jobs it received and can be told +/// to throw, or to block until released, so concurrency and error paths can be exercised. /// public sealed class RecordingJobHandler : IJobHandler { diff --git a/tests/SquidStd.Tests/Workers/WorkerConsumerServiceTests.cs b/tests/SquidStd.Tests/Workers/WorkerConsumerServiceTests.cs index 13217db3..ef8383fb 100644 --- a/tests/SquidStd.Tests/Workers/WorkerConsumerServiceTests.cs +++ b/tests/SquidStd.Tests/Workers/WorkerConsumerServiceTests.cs @@ -1,6 +1,5 @@ using SquidStd.Tests.Workers.Support; using SquidStd.Workers.Abstractions.Data; -using SquidStd.Workers.Data.Config; using SquidStd.Workers.Services; namespace SquidStd.Tests.Workers; @@ -10,7 +9,7 @@ public class WorkerConsumerServiceTests [Fact] public async Task HandleAsync_DropsUnknownJobWithoutThrowing() { - var state = new WorkerState(new WorkersConfig { WorkerId = "w1", MaxConcurrency = 2 }); + var state = new WorkerState(new() { WorkerId = "w1", MaxConcurrency = 2 }); var consumer = Build(state, new RecordingJobHandler("resize")); var ex = await Record.ExceptionAsync(() => consumer.HandleAsync(Job("unknown"), CancellationToken.None)); @@ -22,14 +21,14 @@ public async Task HandleAsync_DropsUnknownJobWithoutThrowing() [Fact] public async Task HandleAsync_NeverExceedsMaxConcurrency() { - var handler = new RecordingJobHandler("resize") { Gate = new TaskCompletionSource() }; - var state = new WorkerState(new WorkersConfig { WorkerId = "w1", MaxConcurrency = 2 }); + var handler = new RecordingJobHandler("resize") { Gate = new() }; + var state = new WorkerState(new() { WorkerId = "w1", MaxConcurrency = 2 }); var consumer = Build(state, handler); // Launch 5 concurrent dispatches against a handler that blocks on its gate. var inFlight = Enumerable.Range(0, 5) - .Select(_ => consumer.HandleAsync(Job("resize"), CancellationToken.None)) - .ToArray(); + .Select(_ => consumer.HandleAsync(Job("resize"), CancellationToken.None)) + .ToArray(); // Give the semaphore time to admit as many as it will, then assert the cap held. await Task.Delay(200); @@ -46,10 +45,11 @@ public async Task HandleAsync_NeverExceedsMaxConcurrency() public async Task HandleAsync_RethrowsHandlerExceptionForRequeue() { var handler = new RecordingJobHandler("resize") { ThrowOnHandle = new InvalidOperationException("boom") }; - var state = new WorkerState(new WorkersConfig { WorkerId = "w1", MaxConcurrency = 2 }); + var state = new WorkerState(new() { WorkerId = "w1", MaxConcurrency = 2 }); var consumer = Build(state, handler); - await Assert.ThrowsAsync(() => consumer.HandleAsync(Job("resize"), CancellationToken.None) + await Assert.ThrowsAsync( + () => consumer.HandleAsync(Job("resize"), CancellationToken.None) ); Assert.Equal(0, state.ActiveJobs); @@ -59,7 +59,7 @@ await Assert.ThrowsAsync(() => consumer.HandleAsync(J public async Task HandleAsync_RunsMatchingHandler() { var handler = new RecordingJobHandler("resize"); - var state = new WorkerState(new WorkersConfig { WorkerId = "w1", MaxConcurrency = 2 }); + var state = new WorkerState(new() { WorkerId = "w1", MaxConcurrency = 2 }); var consumer = Build(state, handler); await consumer.HandleAsync(Job("resize"), CancellationToken.None); @@ -69,12 +69,8 @@ public async Task HandleAsync_RunsMatchingHandler() } private static WorkerConsumerService Build(WorkerState state, params RecordingJobHandler[] handlers) - { - return new WorkerConsumerService(new FakeMessageQueue(), new JobDispatcher(handlers), state, new WorkersConfig()); - } + => new(new FakeMessageQueue(), new JobDispatcher(handlers), state, new()); private static JobRequest Job(string name) - { - return new JobRequest(name, new Dictionary()); - } + => new(name, new Dictionary()); } diff --git a/tests/SquidStd.Tests/Workers/WorkerContractsTests.cs b/tests/SquidStd.Tests/Workers/WorkerContractsTests.cs index 36c3e033..d801fb9d 100644 --- a/tests/SquidStd.Tests/Workers/WorkerContractsTests.cs +++ b/tests/SquidStd.Tests/Workers/WorkerContractsTests.cs @@ -33,15 +33,12 @@ public void WorkerChannels_NamesAreNonEmptyAndDistinct() Assert.NotEqual(WorkerChannels.JobQueue, WorkerChannels.HeartbeatTopic); } - [Theory] - [InlineData(WorkerStatusType.Idle)] - [InlineData(WorkerStatusType.Busy)] - [InlineData(WorkerStatusType.Offline)] + [Theory, InlineData(WorkerStatusType.Idle), InlineData(WorkerStatusType.Busy), InlineData(WorkerStatusType.Offline)] public void WorkerHeartbeat_RoundTrips_EveryStatus(WorkerStatusType status) { var original = new WorkerHeartbeat( "worker-3", - new DateTime(2026, 6, 23, 11, 0, 0, DateTimeKind.Utc), + new(2026, 6, 23, 11, 0, 0, DateTimeKind.Utc), status, 0, 4 @@ -57,7 +54,7 @@ public void WorkerHeartbeat_RoundTrips_PreservingAllFields() { var original = new WorkerHeartbeat( "worker-1", - new DateTime(2026, 6, 23, 10, 0, 0, DateTimeKind.Utc), + new(2026, 6, 23, 10, 0, 0, DateTimeKind.Utc), WorkerStatusType.Busy, 3, 8 @@ -76,8 +73,8 @@ public void WorkerInfo_RoundTrips_PreservingAllFields() WorkerStatusType.Offline, 2, 8, - new DateTime(2026, 6, 23, 9, 0, 0, DateTimeKind.Utc), - new DateTime(2026, 6, 23, 9, 30, 0, DateTimeKind.Utc) + new(2026, 6, 23, 9, 0, 0, DateTimeKind.Utc), + new(2026, 6, 23, 9, 30, 0, DateTimeKind.Utc) ); var restored = Serializer.Deserialize(Serializer.Serialize(original)); diff --git a/tests/SquidStd.Tests/Workers/WorkerHeartbeatServiceTests.cs b/tests/SquidStd.Tests/Workers/WorkerHeartbeatServiceTests.cs index 5c268bde..c52d488e 100644 --- a/tests/SquidStd.Tests/Workers/WorkerHeartbeatServiceTests.cs +++ b/tests/SquidStd.Tests/Workers/WorkerHeartbeatServiceTests.cs @@ -76,6 +76,6 @@ private static (IMessageTopic topic, WorkerState state) NewMessaging(WorkersConf container.RegisterInstance(new EventBusService()); container.AddInMemoryMessaging(); - return (container.Resolve(), new WorkerState(config)); + return (container.Resolve(), new(config)); } } diff --git a/tests/SquidStd.Tests/Workers/WorkerStateTests.cs b/tests/SquidStd.Tests/Workers/WorkerStateTests.cs index ab0b6a86..281e2e5b 100644 --- a/tests/SquidStd.Tests/Workers/WorkerStateTests.cs +++ b/tests/SquidStd.Tests/Workers/WorkerStateTests.cs @@ -1,5 +1,4 @@ using SquidStd.Workers.Abstractions.Types; -using SquidStd.Workers.Data.Config; using SquidStd.Workers.Services; namespace SquidStd.Tests.Workers; @@ -9,7 +8,7 @@ public class WorkerStateTests [Fact] public void MaxConcurrency_FallsBackToProcessorCountWhenNotPositive() { - var state = new WorkerState(new WorkersConfig { WorkerId = "w1", MaxConcurrency = 0 }); + var state = new WorkerState(new() { WorkerId = "w1", MaxConcurrency = 0 }); Assert.Equal(Environment.ProcessorCount, state.MaxConcurrency); } @@ -17,7 +16,7 @@ public void MaxConcurrency_FallsBackToProcessorCountWhenNotPositive() [Fact] public void Status_IsBusyWhileJobsActive() { - var state = new WorkerState(new WorkersConfig { WorkerId = "w1", MaxConcurrency = 4 }); + var state = new WorkerState(new() { WorkerId = "w1", MaxConcurrency = 4 }); state.JobStarted(); state.JobStarted(); @@ -35,7 +34,7 @@ public void Status_IsBusyWhileJobsActive() [Fact] public void Status_IsIdleWhenNoActiveJobs() { - var state = new WorkerState(new WorkersConfig { WorkerId = "w1", MaxConcurrency = 4 }); + var state = new WorkerState(new() { WorkerId = "w1", MaxConcurrency = 4 }); Assert.Equal(WorkerStatusType.Idle, state.Status); Assert.Equal(0, state.ActiveJobs); @@ -44,7 +43,7 @@ public void Status_IsIdleWhenNoActiveJobs() [Fact] public void WorkerId_FallsBackToMachineNameWhenBlank() { - var state = new WorkerState(new WorkersConfig { WorkerId = " ", MaxConcurrency = 4 }); + var state = new WorkerState(new() { WorkerId = " ", MaxConcurrency = 4 }); Assert.Equal(Environment.MachineName, state.WorkerId); } diff --git a/tests/SquidStd.Tests/Workers/WorkersRegistrationExtensionsTests.cs b/tests/SquidStd.Tests/Workers/WorkersRegistrationExtensionsTests.cs index 777f595b..3cb6bb63 100644 --- a/tests/SquidStd.Tests/Workers/WorkersRegistrationExtensionsTests.cs +++ b/tests/SquidStd.Tests/Workers/WorkersRegistrationExtensionsTests.cs @@ -21,7 +21,7 @@ public async Task AddJobHandler_MakesHandlerReachableFromDispatcher() container.AddJobHandler(); var dispatcher = container.Resolve(); - await dispatcher.DispatchAsync(new JobRequest("echo", new Dictionary()), CancellationToken.None); + await dispatcher.DispatchAsync(new("echo", new Dictionary()), CancellationToken.None); Assert.Equal(1, EchoJobHandler.Calls); } diff --git a/tests/SquidStd.Tests/Yaml/YamlUtilsTests.cs b/tests/SquidStd.Tests/Yaml/YamlUtilsTests.cs index dc82d2bb..9dc7a333 100644 --- a/tests/SquidStd.Tests/Yaml/YamlUtilsTests.cs +++ b/tests/SquidStd.Tests/Yaml/YamlUtilsTests.cs @@ -5,13 +5,9 @@ namespace SquidStd.Tests.Yaml; public class YamlUtilsTests { - [Theory] - [InlineData("")] - [InlineData(" ")] + [Theory, InlineData(""), InlineData(" ")] public void Deserialize_NullOrWhitespace_Throws(string yaml) - { - Assert.Throws(() => YamlUtils.Deserialize(yaml)); - } + => Assert.Throws(() => YamlUtils.Deserialize(yaml)); [Fact] public void Deserialize_RuntimeType_ReturnsTypedObject() @@ -31,11 +27,10 @@ public void Deserialize_RuntimeType_ReturnsTypedObject() [Fact] public void DeserializeFromFile_MissingFile_Throws() - { - Assert.Throws(() => - YamlUtils.DeserializeFromFile(Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".yaml")) + => Assert.Throws( + () => + YamlUtils.DeserializeFromFile(Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".yaml")) ); - } [Fact] public void DeserializeSection_ExistingSection_ReturnsTypedObject() @@ -69,9 +64,7 @@ public void DeserializeSection_MissingSection_ReturnsNull() [Fact] public void Serialize_NullObject_Throws() - { - Assert.Throws(() => YamlUtils.Serialize(null!)); - } + => Assert.Throws(() => YamlUtils.Serialize(null!)); [Fact] public void SerializeDeserialize_RoundTrip_PreservesValues() From 5d145a6842ac4dd088d14dd47a39a372ba97a491 Mon Sep 17 00:00:00 2001 From: Tom Date: Tue, 30 Jun 2026 13:54:26 +0200 Subject: [PATCH 24/39] chore(deps): update Redis and AWS SDK packages - StackExchange.Redis 3.0.7 -> 3.0.11 - AWSSDK.SimpleNotificationService / AWSSDK.SQS 4.0.3.8 -> 4.0.100 - AWSSDK.KeyManagementService 4.0.12.9 -> 4.0.100 - AWSSDK.SecretsManager 4.0.5.8 -> 4.0.100 Build clean and full suite green (917) with no new deprecation warnings in the affected adapters. --- src/SquidStd.Caching.Redis/SquidStd.Caching.Redis.csproj | 2 +- src/SquidStd.Messaging.Sqs/SquidStd.Messaging.Sqs.csproj | 4 ++-- src/SquidStd.Secrets.Aws/SquidStd.Secrets.Aws.csproj | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/SquidStd.Caching.Redis/SquidStd.Caching.Redis.csproj b/src/SquidStd.Caching.Redis/SquidStd.Caching.Redis.csproj index a1a8e743..a68bd0f8 100644 --- a/src/SquidStd.Caching.Redis/SquidStd.Caching.Redis.csproj +++ b/src/SquidStd.Caching.Redis/SquidStd.Caching.Redis.csproj @@ -13,7 +13,7 @@ - + diff --git a/src/SquidStd.Messaging.Sqs/SquidStd.Messaging.Sqs.csproj b/src/SquidStd.Messaging.Sqs/SquidStd.Messaging.Sqs.csproj index 1a701a9d..e22c5a8a 100644 --- a/src/SquidStd.Messaging.Sqs/SquidStd.Messaging.Sqs.csproj +++ b/src/SquidStd.Messaging.Sqs/SquidStd.Messaging.Sqs.csproj @@ -13,8 +13,8 @@ - - + + diff --git a/src/SquidStd.Secrets.Aws/SquidStd.Secrets.Aws.csproj b/src/SquidStd.Secrets.Aws/SquidStd.Secrets.Aws.csproj index e9517927..1f28604c 100644 --- a/src/SquidStd.Secrets.Aws/SquidStd.Secrets.Aws.csproj +++ b/src/SquidStd.Secrets.Aws/SquidStd.Secrets.Aws.csproj @@ -8,8 +8,8 @@ - - + + From b5d4ff0abac86dcdeedb016c42ff023a1c9accfe Mon Sep 17 00:00:00 2001 From: Tom Date: Tue, 30 Jun 2026 14:32:23 +0200 Subject: [PATCH 25/39] feat(crypto): add PbkdfCost with Argon2 cost presets for PBE --- .../Password/Data/PbkdfCost.cs | 34 +++++++++++++++++ .../Crypto/Password/PbkdfCostTests.cs | 38 +++++++++++++++++++ 2 files changed, 72 insertions(+) create mode 100644 src/SquidStd.Crypto/Password/Data/PbkdfCost.cs create mode 100644 tests/SquidStd.Tests/Crypto/Password/PbkdfCostTests.cs diff --git a/src/SquidStd.Crypto/Password/Data/PbkdfCost.cs b/src/SquidStd.Crypto/Password/Data/PbkdfCost.cs new file mode 100644 index 00000000..dd4edf95 --- /dev/null +++ b/src/SquidStd.Crypto/Password/Data/PbkdfCost.cs @@ -0,0 +1,34 @@ +namespace SquidStd.Crypto.Password.Data; + +/// Argon2id cost parameters for password-based key derivation. +public sealed class PbkdfCost +{ + /// Argon2id memory cost in KiB. + public int MemoryKib { get; } + + /// Argon2id iteration (time) cost. + public int Iterations { get; } + + /// Argon2id parallelism (lanes). + public int Parallelism { get; } + + public PbkdfCost(int memoryKib, int iterations, int parallelism = 1) + { + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(memoryKib); + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(iterations); + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(parallelism); + + MemoryKib = memoryKib; + Iterations = iterations; + Parallelism = parallelism; + } + + /// Fast, for interactive logins (16 MiB, 2 passes). + public static PbkdfCost Interactive { get; } = new(16 * 1024, 2); + + /// Balanced default (64 MiB, 3 passes). + public static PbkdfCost Moderate { get; } = new(64 * 1024, 3); + + /// High cost for data at rest (256 MiB, 4 passes). + public static PbkdfCost Sensitive { get; } = new(256 * 1024, 4); +} diff --git a/tests/SquidStd.Tests/Crypto/Password/PbkdfCostTests.cs b/tests/SquidStd.Tests/Crypto/Password/PbkdfCostTests.cs new file mode 100644 index 00000000..2f2584ce --- /dev/null +++ b/tests/SquidStd.Tests/Crypto/Password/PbkdfCostTests.cs @@ -0,0 +1,38 @@ +using SquidStd.Crypto.Password.Data; + +namespace SquidStd.Tests.Crypto.Password; + +public class PbkdfCostTests +{ + [Fact] + public void Moderate_HasVaultEquivalentDefaults() + { + Assert.Equal(65536, PbkdfCost.Moderate.MemoryKib); + Assert.Equal(3, PbkdfCost.Moderate.Iterations); + Assert.Equal(1, PbkdfCost.Moderate.Parallelism); + } + + [Fact] + public void Presets_AreOrderedByCost() + { + Assert.True(PbkdfCost.Interactive.MemoryKib < PbkdfCost.Moderate.MemoryKib); + Assert.True(PbkdfCost.Moderate.MemoryKib < PbkdfCost.Sensitive.MemoryKib); + } + + [Fact] + public void Custom_StoresParameters() + { + var cost = new PbkdfCost(memoryKib: 131072, iterations: 4, parallelism: 2); + + Assert.Equal(131072, cost.MemoryKib); + Assert.Equal(4, cost.Iterations); + Assert.Equal(2, cost.Parallelism); + } + + [Theory] + [InlineData(0, 3, 1)] + [InlineData(1024, 0, 1)] + [InlineData(1024, 3, 0)] + public void Custom_RejectsNonPositiveParameters(int memoryKib, int iterations, int parallelism) + => Assert.Throws(() => new PbkdfCost(memoryKib, iterations, parallelism)); +} From 83d933d092ec1d9602d42dece96a60474087c291 Mon Sep 17 00:00:00 2001 From: Tom Date: Tue, 30 Jun 2026 14:35:25 +0200 Subject: [PATCH 26/39] feat(crypto): add versioned AAD-bound envelope codec for PBE --- .../Password/Internal/PasswordEnvelope.cs | 35 +++++++ .../Internal/PasswordEnvelopeCodec.cs | 95 +++++++++++++++++++ .../Password/PasswordEnvelopeCodecTests.cs | 68 +++++++++++++ 3 files changed, 198 insertions(+) create mode 100644 src/SquidStd.Crypto/Password/Internal/PasswordEnvelope.cs create mode 100644 src/SquidStd.Crypto/Password/Internal/PasswordEnvelopeCodec.cs create mode 100644 tests/SquidStd.Tests/Crypto/Password/PasswordEnvelopeCodecTests.cs diff --git a/src/SquidStd.Crypto/Password/Internal/PasswordEnvelope.cs b/src/SquidStd.Crypto/Password/Internal/PasswordEnvelope.cs new file mode 100644 index 00000000..83de8cd1 --- /dev/null +++ b/src/SquidStd.Crypto/Password/Internal/PasswordEnvelope.cs @@ -0,0 +1,35 @@ +namespace SquidStd.Crypto.Password.Internal; + +/// Parsed fields of a password-encryption envelope. +internal sealed class PasswordEnvelope +{ + public byte Version { get; } + public int Iterations { get; } + public int MemoryKib { get; } + public int Parallelism { get; } + public byte[] Salt { get; } + public byte[] Nonce { get; } + public byte[] Tag { get; } + public byte[] Ciphertext { get; } + + public PasswordEnvelope( + byte version, + int iterations, + int memoryKib, + int parallelism, + byte[] salt, + byte[] nonce, + byte[] tag, + byte[] ciphertext + ) + { + Version = version; + Iterations = iterations; + MemoryKib = memoryKib; + Parallelism = parallelism; + Salt = salt; + Nonce = nonce; + Tag = tag; + Ciphertext = ciphertext; + } +} diff --git a/src/SquidStd.Crypto/Password/Internal/PasswordEnvelopeCodec.cs b/src/SquidStd.Crypto/Password/Internal/PasswordEnvelopeCodec.cs new file mode 100644 index 00000000..1c38467b --- /dev/null +++ b/src/SquidStd.Crypto/Password/Internal/PasswordEnvelopeCodec.cs @@ -0,0 +1,95 @@ +using System.Buffers.Binary; + +namespace SquidStd.Crypto.Password.Internal; + +/// Encodes/decodes the self-describing SPBE password-encryption envelope. +internal static class PasswordEnvelopeCodec +{ + public const byte Version = 1; + public const int SaltSize = 16; + public const int NonceSize = 12; + public const int TagSize = 16; + + // magic(4) | version(1) | iterations(4) | memoryKib(4) | parallelism(1) | salt(16) | nonce(12) + public const int AadSize = 4 + 1 + 4 + 4 + 1 + SaltSize + NonceSize; // 42 — authenticated, excludes the tag + public const int HeaderSize = AadSize + TagSize; // 58 — everything before the ciphertext + + private static readonly byte[] _magic = "SPBE"u8.ToArray(); + + public static byte[] BuildAad( + byte version, int iterations, int memoryKib, int parallelism, ReadOnlySpan salt, ReadOnlySpan nonce + ) + { + var aad = new byte[AadSize]; + var span = aad.AsSpan(); + + _magic.CopyTo(span); + span[4] = version; + BinaryPrimitives.WriteInt32BigEndian(span.Slice(5, 4), iterations); + BinaryPrimitives.WriteInt32BigEndian(span.Slice(9, 4), memoryKib); + span[13] = (byte)parallelism; + salt.CopyTo(span.Slice(14, SaltSize)); + nonce.CopyTo(span.Slice(30, NonceSize)); + + return aad; + } + + public static byte[] Encode(PasswordEnvelope envelope) + { + var aad = BuildAad( + envelope.Version, envelope.Iterations, envelope.MemoryKib, envelope.Parallelism, envelope.Salt, envelope.Nonce + ); + + var result = new byte[HeaderSize + envelope.Ciphertext.Length]; + aad.CopyTo(result.AsSpan()); + envelope.Tag.CopyTo(result.AsSpan(AadSize, TagSize)); + envelope.Ciphertext.CopyTo(result.AsSpan(HeaderSize)); + + return result; + } + + public static PasswordEnvelope Decode(byte[] blob) + { + ArgumentNullException.ThrowIfNull(blob); + + if (blob.Length < HeaderSize) + { + throw new InvalidDataException("Encrypted payload is too short."); + } + + var span = blob.AsSpan(); + + if (!span[..4].SequenceEqual(_magic)) + { + throw new InvalidDataException("Encrypted payload has an invalid magic header."); + } + + var version = span[4]; + + if (version != Version) + { + throw new InvalidDataException($"Unsupported encrypted payload version {version}."); + } + + var iterations = BinaryPrimitives.ReadInt32BigEndian(span.Slice(5, 4)); + var memoryKib = BinaryPrimitives.ReadInt32BigEndian(span.Slice(9, 4)); + var parallelism = span[13]; + + if (iterations <= 0 || memoryKib <= 0 || parallelism <= 0) + { + throw new InvalidDataException("Encrypted payload has invalid KDF parameters."); + } + + var salt = span.Slice(14, SaltSize).ToArray(); + var nonce = span.Slice(30, NonceSize).ToArray(); + var tag = span.Slice(AadSize, TagSize).ToArray(); + var ciphertext = span[HeaderSize..].ToArray(); + + return new PasswordEnvelope(version, iterations, memoryKib, parallelism, salt, nonce, tag, ciphertext); + } + + public static ReadOnlySpan AadOf(byte[] blob) + { + return blob.AsSpan(0, AadSize); + } +} diff --git a/tests/SquidStd.Tests/Crypto/Password/PasswordEnvelopeCodecTests.cs b/tests/SquidStd.Tests/Crypto/Password/PasswordEnvelopeCodecTests.cs new file mode 100644 index 00000000..8c6e9b28 --- /dev/null +++ b/tests/SquidStd.Tests/Crypto/Password/PasswordEnvelopeCodecTests.cs @@ -0,0 +1,68 @@ +using SquidStd.Crypto.Password.Internal; + +namespace SquidStd.Tests.Crypto.Password; + +public class PasswordEnvelopeCodecTests +{ + private static PasswordEnvelope Sample() + { + return new PasswordEnvelope( + PasswordEnvelopeCodec.Version, + iterations: 3, + memoryKib: 65536, + parallelism: 1, + salt: Enumerable.Range(0, PasswordEnvelopeCodec.SaltSize).Select(i => (byte)i).ToArray(), + nonce: Enumerable.Range(0, PasswordEnvelopeCodec.NonceSize).Select(i => (byte)(i + 100)).ToArray(), + tag: Enumerable.Range(0, PasswordEnvelopeCodec.TagSize).Select(i => (byte)(i + 200)).ToArray(), + ciphertext: [1, 2, 3, 4, 5]); + } + + [Fact] + public void Encode_Decode_RoundTripsAllFields() + { + var original = Sample(); + + var decoded = PasswordEnvelopeCodec.Decode(PasswordEnvelopeCodec.Encode(original)); + + Assert.Equal(original.Version, decoded.Version); + Assert.Equal(original.Iterations, decoded.Iterations); + Assert.Equal(original.MemoryKib, decoded.MemoryKib); + Assert.Equal(original.Parallelism, decoded.Parallelism); + Assert.Equal(original.Salt, decoded.Salt); + Assert.Equal(original.Nonce, decoded.Nonce); + Assert.Equal(original.Tag, decoded.Tag); + Assert.Equal(original.Ciphertext, decoded.Ciphertext); + } + + [Fact] + public void AadOf_MatchesEverythingBeforeTheTag() + { + var blob = PasswordEnvelopeCodec.Encode(Sample()); + var aad = PasswordEnvelopeCodec.AadOf(blob); + + Assert.Equal(PasswordEnvelopeCodec.AadSize, aad.Length); + Assert.True(aad.SequenceEqual(blob.AsSpan(0, PasswordEnvelopeCodec.AadSize))); + } + + [Fact] + public void Decode_TooShort_Throws() + => Assert.Throws(() => PasswordEnvelopeCodec.Decode(new byte[10])); + + [Fact] + public void Decode_BadMagic_Throws() + { + var blob = PasswordEnvelopeCodec.Encode(Sample()); + blob[0] ^= 0xFF; + + Assert.Throws(() => PasswordEnvelopeCodec.Decode(blob)); + } + + [Fact] + public void Decode_UnknownVersion_Throws() + { + var blob = PasswordEnvelopeCodec.Encode(Sample()); + blob[4] = 99; + + Assert.Throws(() => PasswordEnvelopeCodec.Decode(blob)); + } +} From 223318bb3b3c2be061a75c99c968234304fad625 Mon Sep 17 00:00:00 2001 From: Tom Date: Tue, 30 Jun 2026 14:38:46 +0200 Subject: [PATCH 27/39] feat(crypto): add Argon2id key derivation for PBE --- .../Internal/PasswordKeyDerivation.cs | 28 ++++++++++++++++++ .../Password/PasswordKeyDerivationTests.cs | 29 +++++++++++++++++++ 2 files changed, 57 insertions(+) create mode 100644 src/SquidStd.Crypto/Password/Internal/PasswordKeyDerivation.cs create mode 100644 tests/SquidStd.Tests/Crypto/Password/PasswordKeyDerivationTests.cs diff --git a/src/SquidStd.Crypto/Password/Internal/PasswordKeyDerivation.cs b/src/SquidStd.Crypto/Password/Internal/PasswordKeyDerivation.cs new file mode 100644 index 00000000..4e3e3934 --- /dev/null +++ b/src/SquidStd.Crypto/Password/Internal/PasswordKeyDerivation.cs @@ -0,0 +1,28 @@ +using System.Text; +using Org.BouncyCastle.Crypto.Generators; +using Org.BouncyCastle.Crypto.Parameters; + +namespace SquidStd.Crypto.Password.Internal; + +/// Derives a 32-byte key from a password with Argon2id (v1.3). +internal static class PasswordKeyDerivation +{ + public static byte[] DeriveKey(string password, byte[] salt, int iterations, int memoryKib, int parallelism) + { + var parameters = new Argon2Parameters.Builder(Argon2Parameters.Argon2id) + .WithVersion(Argon2Parameters.Version13) + .WithSalt(salt) + .WithIterations(iterations) + .WithMemoryAsKB(memoryKib) + .WithParallelism(parallelism) + .Build(); + + var generator = new Argon2BytesGenerator(); + generator.Init(parameters); + + var key = new byte[32]; + generator.GenerateBytes(Encoding.UTF8.GetBytes(password), key); + + return key; + } +} diff --git a/tests/SquidStd.Tests/Crypto/Password/PasswordKeyDerivationTests.cs b/tests/SquidStd.Tests/Crypto/Password/PasswordKeyDerivationTests.cs new file mode 100644 index 00000000..fe4b36de --- /dev/null +++ b/tests/SquidStd.Tests/Crypto/Password/PasswordKeyDerivationTests.cs @@ -0,0 +1,29 @@ +using SquidStd.Crypto.Password.Internal; + +namespace SquidStd.Tests.Crypto.Password; + +public class PasswordKeyDerivationTests +{ + private static readonly byte[] Salt = Enumerable.Range(0, 16).Select(i => (byte)i).ToArray(); + + [Fact] + public void DeriveKey_IsDeterministic_AndProduces32Bytes() + { + var a = PasswordKeyDerivation.DeriveKey("pw", Salt, iterations: 2, memoryKib: 8192, parallelism: 1); + var b = PasswordKeyDerivation.DeriveKey("pw", Salt, iterations: 2, memoryKib: 8192, parallelism: 1); + + Assert.Equal(32, a.Length); + Assert.Equal(a, b); + } + + [Fact] + public void DeriveKey_DiffersByPasswordAndSalt() + { + var baseline = PasswordKeyDerivation.DeriveKey("pw", Salt, 2, 8192, 1); + var otherPw = PasswordKeyDerivation.DeriveKey("pw2", Salt, 2, 8192, 1); + var otherSalt = PasswordKeyDerivation.DeriveKey("pw", new byte[16], 2, 8192, 1); + + Assert.NotEqual(baseline, otherPw); + Assert.NotEqual(baseline, otherSalt); + } +} From 2544f22ac0c14135964fcd9fec264c76e9e96a03 Mon Sep 17 00:00:00 2001 From: Tom Date: Tue, 30 Jun 2026 14:41:11 +0200 Subject: [PATCH 28/39] feat(crypto): add PasswordCipher password-based encryption (Argon2id + AES-GCM) - Add PasswordCipher static class: Encrypt/Decrypt byte[], EncryptString/DecryptString (base64) - Add PasswordDecryptionException (sealed, extends CryptographicException) for wrong password or tampered data - AES-256-GCM with AAD-bound authentication; wrong password and ciphertext/header tampering all throw PasswordDecryptionException - Encrypt produces unique blobs per call via random salt + nonce - PbkdfCost defaults to Moderate when omitted --- .../Password/PasswordCipher.cs | 97 +++++++++++++++++++ .../Password/PasswordDecryptionException.cs | 12 +++ .../Crypto/Password/PasswordCipherTests.cs | 73 ++++++++++++++ 3 files changed, 182 insertions(+) create mode 100644 src/SquidStd.Crypto/Password/PasswordCipher.cs create mode 100644 src/SquidStd.Crypto/Password/PasswordDecryptionException.cs create mode 100644 tests/SquidStd.Tests/Crypto/Password/PasswordCipherTests.cs diff --git a/src/SquidStd.Crypto/Password/PasswordCipher.cs b/src/SquidStd.Crypto/Password/PasswordCipher.cs new file mode 100644 index 00000000..a3b5262a --- /dev/null +++ b/src/SquidStd.Crypto/Password/PasswordCipher.cs @@ -0,0 +1,97 @@ +using System.Security.Cryptography; +using System.Text; +using SquidStd.Crypto.Password.Data; +using SquidStd.Crypto.Password.Internal; + +namespace SquidStd.Crypto.Password; + +/// +/// Password-based authenticated encryption. Argon2id derives a key from the password, AES-256-GCM encrypts +/// the payload, and the result is a self-describing envelope carrying the salt, nonce, tag and KDF cost — so +/// decryption needs only the password and the blob. +/// +public static class PasswordCipher +{ + /// Encrypts under . + public static byte[] Encrypt(byte[] plaintext, string password, PbkdfCost? cost = null) + { + ArgumentNullException.ThrowIfNull(plaintext); + ArgumentException.ThrowIfNullOrEmpty(password); + + cost ??= PbkdfCost.Moderate; + + var salt = RandomNumberGenerator.GetBytes(PasswordEnvelopeCodec.SaltSize); + var nonce = RandomNumberGenerator.GetBytes(PasswordEnvelopeCodec.NonceSize); + var key = PasswordKeyDerivation.DeriveKey(password, salt, cost.Iterations, cost.MemoryKib, cost.Parallelism); + + var aad = PasswordEnvelopeCodec.BuildAad( + PasswordEnvelopeCodec.Version, cost.Iterations, cost.MemoryKib, cost.Parallelism, salt, nonce + ); + var ciphertext = new byte[plaintext.Length]; + var tag = new byte[PasswordEnvelopeCodec.TagSize]; + + using (var aes = new AesGcm(key, PasswordEnvelopeCodec.TagSize)) + { + aes.Encrypt(nonce, plaintext, ciphertext, tag, aad); + } + + var envelope = new PasswordEnvelope( + PasswordEnvelopeCodec.Version, cost.Iterations, cost.MemoryKib, cost.Parallelism, salt, nonce, tag, ciphertext + ); + + return PasswordEnvelopeCodec.Encode(envelope); + } + + /// Decrypts an envelope produced by . + public static byte[] Decrypt(byte[] envelope, string password) + { + ArgumentNullException.ThrowIfNull(envelope); + ArgumentException.ThrowIfNullOrEmpty(password); + + var parsed = PasswordEnvelopeCodec.Decode(envelope); + var key = PasswordKeyDerivation.DeriveKey( + password, parsed.Salt, parsed.Iterations, parsed.MemoryKib, parsed.Parallelism + ); + var aad = PasswordEnvelopeCodec.AadOf(envelope); + var plaintext = new byte[parsed.Ciphertext.Length]; + + try + { + using var aes = new AesGcm(key, PasswordEnvelopeCodec.TagSize); + aes.Decrypt(parsed.Nonce, parsed.Ciphertext, parsed.Tag, plaintext, aad); + } + catch (AuthenticationTagMismatchException ex) + { + throw new PasswordDecryptionException("The password is incorrect or the data has been corrupted.", ex); + } + + return plaintext; + } + + /// Encrypts a UTF-8 string and returns the envelope as base64. + public static string EncryptString(string text, string password, PbkdfCost? cost = null) + { + ArgumentNullException.ThrowIfNull(text); + + return Convert.ToBase64String(Encrypt(Encoding.UTF8.GetBytes(text), password, cost)); + } + + /// Decrypts a base64 envelope produced by back to its UTF-8 string. + public static string DecryptString(string base64, string password) + { + ArgumentException.ThrowIfNullOrEmpty(base64); + + byte[] envelope; + + try + { + envelope = Convert.FromBase64String(base64); + } + catch (FormatException ex) + { + throw new InvalidDataException("Input is not valid base64.", ex); + } + + return Encoding.UTF8.GetString(Decrypt(envelope, password)); + } +} diff --git a/src/SquidStd.Crypto/Password/PasswordDecryptionException.cs b/src/SquidStd.Crypto/Password/PasswordDecryptionException.cs new file mode 100644 index 00000000..eedb0628 --- /dev/null +++ b/src/SquidStd.Crypto/Password/PasswordDecryptionException.cs @@ -0,0 +1,12 @@ +using System.Security.Cryptography; + +namespace SquidStd.Crypto.Password; + +/// Thrown when a password is incorrect or the encrypted payload was corrupted or tampered with. +public sealed class PasswordDecryptionException : CryptographicException +{ + public PasswordDecryptionException(string message, Exception innerException) + : base(message, innerException) + { + } +} diff --git a/tests/SquidStd.Tests/Crypto/Password/PasswordCipherTests.cs b/tests/SquidStd.Tests/Crypto/Password/PasswordCipherTests.cs new file mode 100644 index 00000000..82d51043 --- /dev/null +++ b/tests/SquidStd.Tests/Crypto/Password/PasswordCipherTests.cs @@ -0,0 +1,73 @@ +using System.Text; +using SquidStd.Crypto.Password; +using SquidStd.Crypto.Password.Data; +using SquidStd.Crypto.Password.Internal; + +namespace SquidStd.Tests.Crypto.Password; + +public class PasswordCipherTests +{ + // Keep tests fast: a low-cost preset is plenty to exercise the round-trip. + private static readonly PbkdfCost Fast = new(memoryKib: 8192, iterations: 1, parallelism: 1); + + [Fact] + public void Encrypt_Decrypt_RoundTripsBytes() + { + var data = Encoding.UTF8.GetBytes("top secret payload"); + + var blob = PasswordCipher.Encrypt(data, "correct horse", Fast); + + Assert.Equal(data, PasswordCipher.Decrypt(blob, "correct horse")); + } + + [Fact] + public void EncryptString_DecryptString_RoundTrips() + { + var blob = PasswordCipher.EncryptString("ciao mondo", "pw", Fast); + + Assert.Equal("ciao mondo", PasswordCipher.DecryptString(blob, "pw")); + } + + [Fact] + public void Decrypt_WithWrongPassword_Throws() + { + var blob = PasswordCipher.Encrypt([1, 2, 3], "right", Fast); + + Assert.Throws(() => PasswordCipher.Decrypt(blob, "wrong")); + } + + [Fact] + public void Decrypt_TamperedCiphertext_Throws() + { + var blob = PasswordCipher.Encrypt([1, 2, 3, 4], "pw", Fast); + blob[^1] ^= 0xFF; + + Assert.Throws(() => PasswordCipher.Decrypt(blob, "pw")); + } + + [Fact] + public void Decrypt_TamperedHeader_Throws() + { + var blob = PasswordCipher.Encrypt([1, 2, 3, 4], "pw", Fast); + blob[14] ^= 0xFF; // flip a salt byte (part of the AAD) + + Assert.Throws(() => PasswordCipher.Decrypt(blob, "pw")); + } + + [Fact] + public void Encrypt_ProducesDifferentBlobsEachTime() + { + var a = PasswordCipher.Encrypt([9, 9, 9], "pw", Fast); + var b = PasswordCipher.Encrypt([9, 9, 9], "pw", Fast); + + Assert.NotEqual(a, b); + } + + [Fact] + public void DefaultCost_RoundTrips() + { + var blob = PasswordCipher.Encrypt([7], "pw"); // PbkdfCost.Moderate + + Assert.Equal(new byte[] { 7 }, PasswordCipher.Decrypt(blob, "pw")); + } +} From 6bfbec04a11c27ffde14b552bb8d366f88d2686b Mon Sep 17 00:00:00 2001 From: Tom Date: Tue, 30 Jun 2026 14:44:20 +0200 Subject: [PATCH 29/39] docs(crypto): document password-based encryption (PasswordCipher) --- src/SquidStd.Crypto/README.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/SquidStd.Crypto/README.md b/src/SquidStd.Crypto/README.md index cd5d8436..7d0afedf 100644 --- a/src/SquidStd.Crypto/README.md +++ b/src/SquidStd.Crypto/README.md @@ -50,6 +50,30 @@ await keyring.SaveAsync(container.Resolve()); await keyring.LoadAsync(container.Resolve()); ``` +## Password-based encryption + +`PasswordCipher` encrypts a payload under a password — Argon2id derives the key, AES-256-GCM seals the data, +and the result is a self-describing, versioned envelope (salt, nonce, tag and KDF cost are embedded, so +decryption needs only the password and the blob). + +```csharp +using SquidStd.Crypto.Password; +using SquidStd.Crypto.Password.Data; + +byte[] blob = PasswordCipher.Encrypt(payloadBytes, "correct horse battery staple"); +byte[] back = PasswordCipher.Decrypt(blob, "correct horse battery staple"); + +// Text + base64 envelope for storing in config/JSON: +string protectedText = PasswordCipher.EncryptString("a secret", "pw"); +string clear = PasswordCipher.DecryptString(protectedText, "pw"); + +// Tune the Argon2id cost (defaults to PbkdfCost.Moderate): +byte[] strong = PasswordCipher.Encrypt(payloadBytes, "pw", PbkdfCost.Sensitive); +``` + +A wrong password or tampered data raises `PasswordDecryptionException`. For raw-key or app-key/KMS +encryption use `CryptoUtils` / `ISecretProtector` instead. + ## Key types | Type | Purpose | @@ -60,6 +84,8 @@ await keyring.LoadAsync(container.Resolve()); | `FilePgpKeyStore` | One armored `.asc` per key (gpg-interoperable). | | `AesGcmPgpKeyStore` | The whole keyring serialized to a single file, encrypted at rest via `ISecretProtector`. | | `CryptoFileSystem` | `ILockableFileSystem` that encrypts content and names over any `IVirtualFileSystem`. | +| `PasswordCipher` | Password-based encryption: Argon2id + AES-256-GCM with a self-describing envelope. | +| `PbkdfCost` | Argon2id cost presets (Interactive / Moderate / Sensitive) + custom. | ## Key stores From ca8af550c572dc5dae884ea9faa59ff439364c89 Mon Sep 17 00:00:00 2001 From: Tom Date: Tue, 30 Jun 2026 14:50:22 +0200 Subject: [PATCH 30/39] harden(crypto): reject Argon2 parallelism above 255 in PbkdfCost The envelope stores parallelism in a single byte, so a value above 255 would truncate silently and produce an undecryptable blob (the key is derived with the real lane count on encrypt but the envelope records the truncated one). Guard it with ArgumentOutOfRangeException.ThrowIfGreaterThan up front. Add tests for the guard plus empty-plaintext and non-ASCII-password round-trips. --- src/SquidStd.Crypto/Password/Data/PbkdfCost.cs | 4 ++++ .../Crypto/Password/PasswordCipherTests.cs | 16 ++++++++++++++++ .../Crypto/Password/PbkdfCostTests.cs | 4 ++++ 3 files changed, 24 insertions(+) diff --git a/src/SquidStd.Crypto/Password/Data/PbkdfCost.cs b/src/SquidStd.Crypto/Password/Data/PbkdfCost.cs index dd4edf95..1661ee81 100644 --- a/src/SquidStd.Crypto/Password/Data/PbkdfCost.cs +++ b/src/SquidStd.Crypto/Password/Data/PbkdfCost.cs @@ -18,6 +18,10 @@ public PbkdfCost(int memoryKib, int iterations, int parallelism = 1) ArgumentOutOfRangeException.ThrowIfNegativeOrZero(iterations); ArgumentOutOfRangeException.ThrowIfNegativeOrZero(parallelism); + // The envelope stores parallelism in a single byte, so reject values that would truncate and + // silently produce an undecryptable blob. Real Argon2 lane counts are far below this. + ArgumentOutOfRangeException.ThrowIfGreaterThan(parallelism, 255); + MemoryKib = memoryKib; Iterations = iterations; Parallelism = parallelism; diff --git a/tests/SquidStd.Tests/Crypto/Password/PasswordCipherTests.cs b/tests/SquidStd.Tests/Crypto/Password/PasswordCipherTests.cs index 82d51043..46a3abc2 100644 --- a/tests/SquidStd.Tests/Crypto/Password/PasswordCipherTests.cs +++ b/tests/SquidStd.Tests/Crypto/Password/PasswordCipherTests.cs @@ -70,4 +70,20 @@ public void DefaultCost_RoundTrips() Assert.Equal(new byte[] { 7 }, PasswordCipher.Decrypt(blob, "pw")); } + + [Fact] + public void Encrypt_EmptyPlaintext_RoundTrips() + { + var blob = PasswordCipher.Encrypt([], "pw", Fast); + + Assert.Equal([], PasswordCipher.Decrypt(blob, "pw")); + } + + [Fact] + public void EncryptString_NonAsciiPassword_RoundTrips() + { + var blob = PasswordCipher.EncryptString("payload", "pâsswörd-日本語-🔐", Fast); + + Assert.Equal("payload", PasswordCipher.DecryptString(blob, "pâsswörd-日本語-🔐")); + } } diff --git a/tests/SquidStd.Tests/Crypto/Password/PbkdfCostTests.cs b/tests/SquidStd.Tests/Crypto/Password/PbkdfCostTests.cs index 2f2584ce..0c626d76 100644 --- a/tests/SquidStd.Tests/Crypto/Password/PbkdfCostTests.cs +++ b/tests/SquidStd.Tests/Crypto/Password/PbkdfCostTests.cs @@ -35,4 +35,8 @@ public void Custom_StoresParameters() [InlineData(1024, 3, 0)] public void Custom_RejectsNonPositiveParameters(int memoryKib, int iterations, int parallelism) => Assert.Throws(() => new PbkdfCost(memoryKib, iterations, parallelism)); + + [Fact] + public void Custom_RejectsParallelismAbove255() + => Assert.Throws(() => new PbkdfCost(1024, 3, 256)); } From 54f9aebc19a35110dab28e5a019eb40c54477d83 Mon Sep 17 00:00:00 2001 From: Tom Date: Tue, 30 Jun 2026 15:27:40 +0200 Subject: [PATCH 31/39] feat(vfs): add DeferredWriteStream for deferred OpenWrite backends --- .../DeferredWriteStream.cs | 42 +++++++++++++++++ .../Vfs/DeferredWriteStreamTests.cs | 45 +++++++++++++++++++ 2 files changed, 87 insertions(+) create mode 100644 src/SquidStd.Vfs.Abstractions/DeferredWriteStream.cs create mode 100644 tests/SquidStd.Tests/Vfs/DeferredWriteStreamTests.cs diff --git a/src/SquidStd.Vfs.Abstractions/DeferredWriteStream.cs b/src/SquidStd.Vfs.Abstractions/DeferredWriteStream.cs new file mode 100644 index 00000000..8a8ee284 --- /dev/null +++ b/src/SquidStd.Vfs.Abstractions/DeferredWriteStream.cs @@ -0,0 +1,42 @@ +namespace SquidStd.Vfs.Abstractions; + +/// +/// A writable in-memory stream that flushes its accumulated bytes through a callback exactly once on +/// disposal. Backs OpenWriteAsync for filesystems that persist the whole payload at close time. +/// +public sealed class DeferredWriteStream : MemoryStream +{ + private readonly Func _onFlush; + private readonly CancellationToken _cancellationToken; + private bool _flushed; + + public DeferredWriteStream(Func onFlush, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(onFlush); + + _onFlush = onFlush; + _cancellationToken = cancellationToken; + } + + public override async ValueTask DisposeAsync() + { + if (!_flushed) + { + _flushed = true; + await _onFlush(ToArray(), _cancellationToken).ConfigureAwait(false); + } + + await base.DisposeAsync().ConfigureAwait(false); + } + + protected override void Dispose(bool disposing) + { + if (disposing && !_flushed) + { + _flushed = true; + _onFlush(ToArray(), _cancellationToken).AsTask().GetAwaiter().GetResult(); + } + + base.Dispose(disposing); + } +} diff --git a/tests/SquidStd.Tests/Vfs/DeferredWriteStreamTests.cs b/tests/SquidStd.Tests/Vfs/DeferredWriteStreamTests.cs new file mode 100644 index 00000000..11235a86 --- /dev/null +++ b/tests/SquidStd.Tests/Vfs/DeferredWriteStreamTests.cs @@ -0,0 +1,45 @@ +using SquidStd.Vfs.Abstractions; + +namespace SquidStd.Tests.Vfs; + +public class DeferredWriteStreamTests +{ + [Fact] + public async Task DisposeAsync_FlushesWrittenBytesOnce() + { + byte[]? flushed = null; + var flushes = 0; + + var stream = new DeferredWriteStream((bytes, _) => + { + flushed = bytes; + flushes++; + + return ValueTask.CompletedTask; + }); + + await stream.WriteAsync(new byte[] { 1, 2, 3 }); + await stream.DisposeAsync(); + + Assert.Equal(new byte[] { 1, 2, 3 }, flushed); + Assert.Equal(1, flushes); + } + + [Fact] + public void Dispose_Synchronous_AlsoFlushes() + { + byte[]? flushed = null; + + using (var stream = new DeferredWriteStream((bytes, _) => + { + flushed = bytes; + + return ValueTask.CompletedTask; + })) + { + stream.Write(new byte[] { 9 }, 0, 1); + } + + Assert.Equal(new byte[] { 9 }, flushed); + } +} From 04dcb41f1c07b173c7f6a84d3d56cc865bb5d9cd Mon Sep 17 00:00:00 2001 From: Tom Date: Tue, 30 Jun 2026 15:29:49 +0200 Subject: [PATCH 32/39] feat(vfs): add ReadOnlyFileSystem decorator --- .../Services/ReadOnlyFileSystem.cs | 40 +++++++++++++++++++ .../Vfs/ReadOnlyFileSystemTests.cs | 34 ++++++++++++++++ 2 files changed, 74 insertions(+) create mode 100644 src/SquidStd.Vfs/Services/ReadOnlyFileSystem.cs create mode 100644 tests/SquidStd.Tests/Vfs/ReadOnlyFileSystemTests.cs diff --git a/src/SquidStd.Vfs/Services/ReadOnlyFileSystem.cs b/src/SquidStd.Vfs/Services/ReadOnlyFileSystem.cs new file mode 100644 index 00000000..9dc6e1a7 --- /dev/null +++ b/src/SquidStd.Vfs/Services/ReadOnlyFileSystem.cs @@ -0,0 +1,40 @@ +using SquidStd.Vfs.Abstractions.Data; +using SquidStd.Vfs.Abstractions.Interfaces; + +namespace SquidStd.Vfs.Services; + +/// Wraps a filesystem and rejects every mutation. +public sealed class ReadOnlyFileSystem : IVirtualFileSystem +{ + private const string Message = "This filesystem is read-only."; + + private readonly IVirtualFileSystem _inner; + + public ReadOnlyFileSystem(IVirtualFileSystem inner) + { + ArgumentNullException.ThrowIfNull(inner); + + _inner = inner; + } + + public ValueTask ExistsAsync(string path, CancellationToken cancellationToken = default) + => _inner.ExistsAsync(path, cancellationToken); + + public ValueTask ReadAllBytesAsync(string path, CancellationToken cancellationToken = default) + => _inner.ReadAllBytesAsync(path, cancellationToken); + + public ValueTask WriteAllBytesAsync(string path, ReadOnlyMemory data, CancellationToken cancellationToken = default) + => throw new InvalidOperationException(Message); + + public Task OpenReadAsync(string path, CancellationToken cancellationToken = default) + => _inner.OpenReadAsync(path, cancellationToken); + + public Task OpenWriteAsync(string path, CancellationToken cancellationToken = default) + => throw new InvalidOperationException(Message); + + public ValueTask DeleteAsync(string path, CancellationToken cancellationToken = default) + => throw new InvalidOperationException(Message); + + public IAsyncEnumerable ListAsync(string? prefix = null, CancellationToken cancellationToken = default) + => _inner.ListAsync(prefix, cancellationToken); +} diff --git a/tests/SquidStd.Tests/Vfs/ReadOnlyFileSystemTests.cs b/tests/SquidStd.Tests/Vfs/ReadOnlyFileSystemTests.cs new file mode 100644 index 00000000..5e3ce561 --- /dev/null +++ b/tests/SquidStd.Tests/Vfs/ReadOnlyFileSystemTests.cs @@ -0,0 +1,34 @@ +using SquidStd.Vfs.Abstractions.Interfaces; +using SquidStd.Vfs.Services; + +namespace SquidStd.Tests.Vfs; + +public class ReadOnlyFileSystemTests +{ + private static async Task SeededInnerAsync() + { + var inner = new InMemoryFileSystem(); + await inner.WriteAllBytesAsync("a.txt", new byte[] { 1 }); + + return inner; + } + + [Fact] + public async Task Reads_Delegate() + { + var fs = new ReadOnlyFileSystem(await SeededInnerAsync()); + + Assert.True(await fs.ExistsAsync("a.txt")); + Assert.Equal(new byte[] { 1 }, await fs.ReadAllBytesAsync("a.txt")); + } + + [Fact] + public async Task Writes_Throw() + { + var fs = new ReadOnlyFileSystem(await SeededInnerAsync()); + + await Assert.ThrowsAsync(async () => await fs.WriteAllBytesAsync("b.txt", new byte[] { 2 })); + await Assert.ThrowsAsync(async () => await fs.DeleteAsync("a.txt")); + await Assert.ThrowsAsync(async () => await fs.OpenWriteAsync("b.txt")); + } +} From fdf5b4efd156f826f35fae3f3b7d2f1a91dd3207 Mon Sep 17 00:00:00 2001 From: Tom Date: Tue, 30 Jun 2026 15:32:21 +0200 Subject: [PATCH 33/39] feat(vfs): add ScopedFileSystem decorator --- src/SquidStd.Vfs/Services/ScopedFileSystem.cs | 62 +++++++++++++++++++ .../Vfs/ScopedFileSystemTests.cs | 53 ++++++++++++++++ 2 files changed, 115 insertions(+) create mode 100644 src/SquidStd.Vfs/Services/ScopedFileSystem.cs create mode 100644 tests/SquidStd.Tests/Vfs/ScopedFileSystemTests.cs diff --git a/src/SquidStd.Vfs/Services/ScopedFileSystem.cs b/src/SquidStd.Vfs/Services/ScopedFileSystem.cs new file mode 100644 index 00000000..03fbd0f7 --- /dev/null +++ b/src/SquidStd.Vfs/Services/ScopedFileSystem.cs @@ -0,0 +1,62 @@ +using System.Runtime.CompilerServices; +using SquidStd.Vfs.Abstractions; +using SquidStd.Vfs.Abstractions.Data; +using SquidStd.Vfs.Abstractions.Interfaces; + +namespace SquidStd.Vfs.Services; + +/// Roots an inner filesystem at a fixed path prefix (a chroot-like view). +public sealed class ScopedFileSystem : IVirtualFileSystem +{ + private readonly IVirtualFileSystem _inner; + private readonly string _prefix; + + public ScopedFileSystem(IVirtualFileSystem inner, string prefix) + { + ArgumentNullException.ThrowIfNull(inner); + ArgumentException.ThrowIfNullOrWhiteSpace(prefix); + + _inner = inner; + _prefix = VfsPath.Normalize(prefix); + } + + public ValueTask ExistsAsync(string path, CancellationToken cancellationToken = default) + => _inner.ExistsAsync(Scope(path), cancellationToken); + + public ValueTask ReadAllBytesAsync(string path, CancellationToken cancellationToken = default) + => _inner.ReadAllBytesAsync(Scope(path), cancellationToken); + + public ValueTask WriteAllBytesAsync(string path, ReadOnlyMemory data, CancellationToken cancellationToken = default) + => _inner.WriteAllBytesAsync(Scope(path), data, cancellationToken); + + public Task OpenReadAsync(string path, CancellationToken cancellationToken = default) + => _inner.OpenReadAsync(Scope(path), cancellationToken); + + public Task OpenWriteAsync(string path, CancellationToken cancellationToken = default) + => _inner.OpenWriteAsync(Scope(path), cancellationToken); + + public ValueTask DeleteAsync(string path, CancellationToken cancellationToken = default) + => _inner.DeleteAsync(Scope(path), cancellationToken); + + public async IAsyncEnumerable ListAsync(string? prefix = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + var scopedPrefix = string.IsNullOrEmpty(prefix) ? _prefix : Scope(prefix); + var boundary = _prefix + "/"; + + await foreach (var entry in _inner.ListAsync(scopedPrefix, cancellationToken).ConfigureAwait(false)) + { + if (!entry.Path.StartsWith(boundary, StringComparison.Ordinal)) + { + continue; // a sibling scope sharing a name prefix (e.g. "tenant10" vs "tenant1") — not ours + } + + yield return entry with { Path = Unscope(entry.Path) }; + } + } + + private string Scope(string path) + => VfsPath.Normalize(_prefix + "/" + path); + + private string Unscope(string path) + => path.StartsWith(_prefix + "/", StringComparison.Ordinal) ? path[(_prefix.Length + 1)..] : path; +} diff --git a/tests/SquidStd.Tests/Vfs/ScopedFileSystemTests.cs b/tests/SquidStd.Tests/Vfs/ScopedFileSystemTests.cs new file mode 100644 index 00000000..9b2d8668 --- /dev/null +++ b/tests/SquidStd.Tests/Vfs/ScopedFileSystemTests.cs @@ -0,0 +1,53 @@ +using SquidStd.Vfs.Services; + +namespace SquidStd.Tests.Vfs; + +public class ScopedFileSystemTests +{ + [Fact] + public async Task Scopes_ReadWrite_UnderPrefix() + { + var inner = new InMemoryFileSystem(); + var fs = new ScopedFileSystem(inner, "tenant1"); + + await fs.WriteAllBytesAsync("docs/a.txt", new byte[] { 1 }); + + Assert.True(await inner.ExistsAsync("tenant1/docs/a.txt")); // stored under the prefix + Assert.Equal(new byte[] { 1 }, await fs.ReadAllBytesAsync("docs/a.txt")); + } + + [Fact] + public async Task List_StripsPrefix_FromReturnedPaths() + { + var inner = new InMemoryFileSystem(); + var fs = new ScopedFileSystem(inner, "tenant1"); + await fs.WriteAllBytesAsync("docs/a.txt", new byte[] { 1 }); + await inner.WriteAllBytesAsync("tenant2/secret.txt", new byte[] { 9 }); // outside the scope + + var paths = new List(); + await foreach (var e in fs.ListAsync()) + { + paths.Add(e.Path); + } + + Assert.Contains("docs/a.txt", paths); + Assert.DoesNotContain(paths, p => p.Contains("tenant2")); + } + + [Fact] + public async Task List_DoesNotLeak_SiblingScopeSharingNamePrefix() + { + var inner = new InMemoryFileSystem(); + var fs = new ScopedFileSystem(inner, "tenant1"); + await fs.WriteAllBytesAsync("a.txt", new byte[] { 1 }); // -> tenant1/a.txt + await inner.WriteAllBytesAsync("tenant10/b.txt", new byte[] { 9 }); // sibling scope, shares the "tenant1" prefix + + var paths = new List(); + await foreach (var e in fs.ListAsync()) + { + paths.Add(e.Path); + } + + Assert.Equal(new[] { "a.txt" }, paths); // only our scope, and stripped + } +} From def154808bf1908e7762ee3ab180f41c4e16a349 Mon Sep 17 00:00:00 2001 From: Tom Date: Tue, 30 Jun 2026 15:36:51 +0200 Subject: [PATCH 34/39] feat(vfs): add OverlayFileSystem decorator --- .../Services/OverlayFileSystem.cs | 63 ++++++++++++++++++ .../Vfs/OverlayFileSystemTests.cs | 66 +++++++++++++++++++ 2 files changed, 129 insertions(+) create mode 100644 src/SquidStd.Vfs/Services/OverlayFileSystem.cs create mode 100644 tests/SquidStd.Tests/Vfs/OverlayFileSystemTests.cs diff --git a/src/SquidStd.Vfs/Services/OverlayFileSystem.cs b/src/SquidStd.Vfs/Services/OverlayFileSystem.cs new file mode 100644 index 00000000..8b319c76 --- /dev/null +++ b/src/SquidStd.Vfs/Services/OverlayFileSystem.cs @@ -0,0 +1,63 @@ +using System.Runtime.CompilerServices; +using SquidStd.Vfs.Abstractions.Data; +using SquidStd.Vfs.Abstractions.Interfaces; + +namespace SquidStd.Vfs.Services; + +/// Reads from an overlay first then a base; writes and deletes affect only the overlay. +public sealed class OverlayFileSystem : IVirtualFileSystem +{ + private readonly IVirtualFileSystem _base; + private readonly IVirtualFileSystem _overlay; + + public OverlayFileSystem(IVirtualFileSystem baseFileSystem, IVirtualFileSystem overlay) + { + ArgumentNullException.ThrowIfNull(baseFileSystem); + ArgumentNullException.ThrowIfNull(overlay); + + _base = baseFileSystem; + _overlay = overlay; + } + + public async ValueTask ExistsAsync(string path, CancellationToken cancellationToken = default) + => await _overlay.ExistsAsync(path, cancellationToken).ConfigureAwait(false) + || await _base.ExistsAsync(path, cancellationToken).ConfigureAwait(false); + + public async ValueTask ReadAllBytesAsync(string path, CancellationToken cancellationToken = default) + => await _overlay.ReadAllBytesAsync(path, cancellationToken).ConfigureAwait(false) + ?? await _base.ReadAllBytesAsync(path, cancellationToken).ConfigureAwait(false); + + public ValueTask WriteAllBytesAsync(string path, ReadOnlyMemory data, CancellationToken cancellationToken = default) + => _overlay.WriteAllBytesAsync(path, data, cancellationToken); + + public async Task OpenReadAsync(string path, CancellationToken cancellationToken = default) + => await _overlay.ExistsAsync(path, cancellationToken).ConfigureAwait(false) + ? await _overlay.OpenReadAsync(path, cancellationToken).ConfigureAwait(false) + : await _base.OpenReadAsync(path, cancellationToken).ConfigureAwait(false); + + public Task OpenWriteAsync(string path, CancellationToken cancellationToken = default) + => _overlay.OpenWriteAsync(path, cancellationToken); + + public ValueTask DeleteAsync(string path, CancellationToken cancellationToken = default) + => _overlay.DeleteAsync(path, cancellationToken); + + public async IAsyncEnumerable ListAsync(string? prefix = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + var seen = new HashSet(StringComparer.Ordinal); + + await foreach (var entry in _overlay.ListAsync(prefix, cancellationToken).ConfigureAwait(false)) + { + seen.Add(entry.Path); + + yield return entry; + } + + await foreach (var entry in _base.ListAsync(prefix, cancellationToken).ConfigureAwait(false)) + { + if (seen.Add(entry.Path)) + { + yield return entry; + } + } + } +} diff --git a/tests/SquidStd.Tests/Vfs/OverlayFileSystemTests.cs b/tests/SquidStd.Tests/Vfs/OverlayFileSystemTests.cs new file mode 100644 index 00000000..b3bd38f1 --- /dev/null +++ b/tests/SquidStd.Tests/Vfs/OverlayFileSystemTests.cs @@ -0,0 +1,66 @@ +using SquidStd.Vfs.Services; + +namespace SquidStd.Tests.Vfs; + +public class OverlayFileSystemTests +{ + [Fact] + public async Task Read_PrefersOverlay_ThenBase() + { + var @base = new InMemoryFileSystem(); + var overlay = new InMemoryFileSystem(); + await @base.WriteAllBytesAsync("a.txt", new byte[] { 1 }); + await @base.WriteAllBytesAsync("b.txt", new byte[] { 2 }); + await overlay.WriteAllBytesAsync("a.txt", new byte[] { 99 }); + var fs = new OverlayFileSystem(@base, overlay); + + Assert.Equal(new byte[] { 99 }, await fs.ReadAllBytesAsync("a.txt")); // overlay shadows base + Assert.Equal(new byte[] { 2 }, await fs.ReadAllBytesAsync("b.txt")); // falls through to base + } + + [Fact] + public async Task Write_GoesToOverlay_BaseUntouched() + { + var @base = new InMemoryFileSystem(); + var overlay = new InMemoryFileSystem(); + var fs = new OverlayFileSystem(@base, overlay); + + await fs.WriteAllBytesAsync("c.txt", new byte[] { 7 }); + + Assert.True(await overlay.ExistsAsync("c.txt")); + Assert.False(await @base.ExistsAsync("c.txt")); + } + + [Fact] + public async Task Delete_BaseOnlyFile_ReturnsFalse() + { + var @base = new InMemoryFileSystem(); + var overlay = new InMemoryFileSystem(); + await @base.WriteAllBytesAsync("a.txt", new byte[] { 1 }); + var fs = new OverlayFileSystem(@base, overlay); + + Assert.False(await fs.DeleteAsync("a.txt")); // overlay has nothing to delete; base untouched + Assert.True(await @base.ExistsAsync("a.txt")); + } + + [Fact] + public async Task List_IsUnion_OverlayShadowsBase() + { + var @base = new InMemoryFileSystem(); + var overlay = new InMemoryFileSystem(); + await @base.WriteAllBytesAsync("a.txt", new byte[] { 1 }); + await @base.WriteAllBytesAsync("b.txt", new byte[] { 2 }); + await overlay.WriteAllBytesAsync("a.txt", new byte[] { 99 }); + var fs = new OverlayFileSystem(@base, overlay); + + var paths = new List(); + await foreach (var e in fs.ListAsync()) + { + paths.Add(e.Path); + } + + Assert.Equal(2, paths.Count); // a.txt once + b.txt + Assert.Contains("a.txt", paths); + Assert.Contains("b.txt", paths); + } +} From 227795ef22e00fb5c3b8788a275a701b7d23d918 Mon Sep 17 00:00:00 2001 From: Tom Date: Tue, 30 Jun 2026 15:38:59 +0200 Subject: [PATCH 35/39] feat(vfs): add CachingFileSystem with offline read fallback --- .../Services/CachingFileSystem.cs | 142 ++++++++++++++++++ .../Vfs/CachingFileSystemTests.cs | 97 ++++++++++++ 2 files changed, 239 insertions(+) create mode 100644 src/SquidStd.Vfs/Services/CachingFileSystem.cs create mode 100644 tests/SquidStd.Tests/Vfs/CachingFileSystemTests.cs diff --git a/src/SquidStd.Vfs/Services/CachingFileSystem.cs b/src/SquidStd.Vfs/Services/CachingFileSystem.cs new file mode 100644 index 00000000..91f58215 --- /dev/null +++ b/src/SquidStd.Vfs/Services/CachingFileSystem.cs @@ -0,0 +1,142 @@ +using System.Net.Http; +using System.Runtime.CompilerServices; +using SquidStd.Vfs.Abstractions; +using SquidStd.Vfs.Abstractions.Data; +using SquidStd.Vfs.Abstractions.Interfaces; + +namespace SquidStd.Vfs.Services; + +/// +/// Read-through cache over a remote filesystem. Reads prefer the remote and refresh the cache; on a +/// transport failure they fall back to the (possibly stale) cache. Writes and deletes are write-through +/// (remote then cache) and surface the remote's error when it is unreachable. +/// +public sealed class CachingFileSystem : IVirtualFileSystem +{ + private readonly IVirtualFileSystem _remote; + private readonly IVirtualFileSystem _cache; + + public CachingFileSystem(IVirtualFileSystem remote, IVirtualFileSystem cache) + { + ArgumentNullException.ThrowIfNull(remote); + ArgumentNullException.ThrowIfNull(cache); + + _remote = remote; + _cache = cache; + } + + public async ValueTask ExistsAsync(string path, CancellationToken cancellationToken = default) + { + try + { + return await _remote.ExistsAsync(path, cancellationToken).ConfigureAwait(false); + } + catch (Exception ex) when (IsTransient(ex)) + { + return await _cache.ExistsAsync(path, cancellationToken).ConfigureAwait(false); + } + } + + public async ValueTask ReadAllBytesAsync(string path, CancellationToken cancellationToken = default) + { + try + { + var data = await _remote.ReadAllBytesAsync(path, cancellationToken).ConfigureAwait(false); + + if (data is not null) + { + await _cache.WriteAllBytesAsync(path, data, cancellationToken).ConfigureAwait(false); + } + + return data; + } + catch (Exception ex) when (IsTransient(ex)) + { + return await _cache.ReadAllBytesAsync(path, cancellationToken).ConfigureAwait(false); + } + } + + public async ValueTask WriteAllBytesAsync(string path, ReadOnlyMemory data, CancellationToken cancellationToken = default) + { + await _remote.WriteAllBytesAsync(path, data, cancellationToken).ConfigureAwait(false); + await _cache.WriteAllBytesAsync(path, data, cancellationToken).ConfigureAwait(false); + } + + public async Task OpenReadAsync(string path, CancellationToken cancellationToken = default) + { + var data = await ReadAllBytesAsync(path, cancellationToken).ConfigureAwait(false) + ?? throw new FileNotFoundException($"No file at '{path}'.", path); + + return new MemoryStream(data, writable: false); + } + + public Task OpenWriteAsync(string path, CancellationToken cancellationToken = default) + => Task.FromResult(new DeferredWriteStream((bytes, ct) => WriteAllBytesAsync(path, bytes, ct), cancellationToken)); + + public async ValueTask DeleteAsync(string path, CancellationToken cancellationToken = default) + { + var removed = await _remote.DeleteAsync(path, cancellationToken).ConfigureAwait(false); + await _cache.DeleteAsync(path, cancellationToken).ConfigureAwait(false); + + return removed; + } + + public async IAsyncEnumerable ListAsync(string? prefix = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + IAsyncEnumerator? enumerator = null; + var fellBack = false; + + try + { + try + { + enumerator = _remote.ListAsync(prefix, cancellationToken).GetAsyncEnumerator(cancellationToken); + } + catch (Exception ex) when (IsTransient(ex)) + { + fellBack = true; + } + + while (!fellBack) + { + VfsEntry current; + + try + { + if (!await enumerator!.MoveNextAsync().ConfigureAwait(false)) + { + break; + } + + current = enumerator.Current; + } + catch (Exception ex) when (IsTransient(ex)) + { + fellBack = true; + + break; + } + + yield return current; + } + } + finally + { + if (enumerator is not null) + { + await enumerator.DisposeAsync().ConfigureAwait(false); + } + } + + if (fellBack) + { + await foreach (var entry in _cache.ListAsync(prefix, cancellationToken).ConfigureAwait(false)) + { + yield return entry; + } + } + } + + private static bool IsTransient(Exception ex) + => ex is HttpRequestException or IOException or TimeoutException; +} diff --git a/tests/SquidStd.Tests/Vfs/CachingFileSystemTests.cs b/tests/SquidStd.Tests/Vfs/CachingFileSystemTests.cs new file mode 100644 index 00000000..71b6ad2b --- /dev/null +++ b/tests/SquidStd.Tests/Vfs/CachingFileSystemTests.cs @@ -0,0 +1,97 @@ +using System.Net.Http; +using SquidStd.Vfs.Abstractions.Data; +using SquidStd.Vfs.Abstractions.Interfaces; +using SquidStd.Vfs.Services; + +namespace SquidStd.Tests.Vfs; + +public class CachingFileSystemTests +{ + // An InMemoryFileSystem-backed remote with a switch that makes every op throw a transport error. + private sealed class FlakyRemote : IVirtualFileSystem + { + private readonly InMemoryFileSystem _inner = new(); + + public bool Offline { get; set; } + + private void Guard() + { + if (Offline) + { + throw new HttpRequestException("offline"); + } + } + + public ValueTask ExistsAsync(string path, CancellationToken ct = default) { Guard(); return _inner.ExistsAsync(path, ct); } + public ValueTask ReadAllBytesAsync(string path, CancellationToken ct = default) { Guard(); return _inner.ReadAllBytesAsync(path, ct); } + public ValueTask WriteAllBytesAsync(string path, ReadOnlyMemory data, CancellationToken ct = default) { Guard(); return _inner.WriteAllBytesAsync(path, data, ct); } + public Task OpenReadAsync(string path, CancellationToken ct = default) { Guard(); return _inner.OpenReadAsync(path, ct); } + public Task OpenWriteAsync(string path, CancellationToken ct = default) { Guard(); return _inner.OpenWriteAsync(path, ct); } + public ValueTask DeleteAsync(string path, CancellationToken ct = default) { Guard(); return _inner.DeleteAsync(path, ct); } + public IAsyncEnumerable ListAsync(string? prefix = null, CancellationToken ct = default) { Guard(); return _inner.ListAsync(prefix, ct); } + } + + [Fact] + public async Task Read_PopulatesCache_AndServesFromCacheWhenOffline() + { + var remote = new FlakyRemote(); + var cache = new InMemoryFileSystem(); + var fs = new CachingFileSystem(remote, cache); + + await remote.WriteAllBytesAsync("a.txt", new byte[] { 1 }); + + Assert.Equal(new byte[] { 1 }, await fs.ReadAllBytesAsync("a.txt")); // remote read populates cache + Assert.True(await cache.ExistsAsync("a.txt")); + + remote.Offline = true; + Assert.Equal(new byte[] { 1 }, await fs.ReadAllBytesAsync("a.txt")); // served from cache while offline + } + + [Fact] + public async Task Write_IsWriteThrough_AndFailsOffline() + { + var remote = new FlakyRemote(); + var cache = new InMemoryFileSystem(); + var fs = new CachingFileSystem(remote, cache); + + await fs.WriteAllBytesAsync("a.txt", new byte[] { 5 }); + Assert.True(await remote.ExistsAsync("a.txt")); + Assert.True(await cache.ExistsAsync("a.txt")); + + remote.Offline = true; + await Assert.ThrowsAsync(async () => await fs.WriteAllBytesAsync("b.txt", new byte[] { 6 })); + } + + [Fact] + public async Task List_FallsBackToCache_WhenOffline() + { + var remote = new FlakyRemote(); + var cache = new InMemoryFileSystem(); + var fs = new CachingFileSystem(remote, cache); + + await remote.WriteAllBytesAsync("a.txt", new byte[] { 1 }); + await fs.ReadAllBytesAsync("a.txt"); // populate the cache while online + + remote.Offline = true; // remote.ListAsync now throws eagerly (HttpRequestException) + + var paths = new List(); + await foreach (var e in fs.ListAsync()) + { + paths.Add(e.Path); + } + + Assert.Contains("a.txt", paths); // served from cache despite the remote being offline + } + + [Fact] + public async Task Read_MissingFile_DoesNotFallBackToCache() + { + var remote = new FlakyRemote(); + var cache = new InMemoryFileSystem(); + await cache.WriteAllBytesAsync("ghost.txt", new byte[] { 7 }); // stale cache entry + var fs = new CachingFileSystem(remote, cache); + + // Remote is online and has no such file => null, NOT the stale cache copy. + Assert.Null(await fs.ReadAllBytesAsync("ghost.txt")); + } +} From 501847299e7f65acd978daa96618933bb670351e Mon Sep 17 00:00:00 2001 From: Tom Date: Tue, 30 Jun 2026 15:52:21 +0200 Subject: [PATCH 36/39] feat(vfs): add S3-compatible filesystem backend (SquidStd.Vfs.S3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - new project SquidStd.Vfs.S3 (net10.0, PublishNuget=true, Minio 7.0.0) - S3FileSystemOptions: Aws (AwsConfigEntry) + Bucket, mirrors S3StorageOptions shape - S3FileSystem: IVirtualFileSystem + IDisposable over MinIO client - lazy EnsureBucketAsync (BucketExistsArgs / MakeBucketArgs) with SemaphoreSlim - ExistsAsync via StatObjectArgs, ObjectNotFoundException → false - ReadAllBytesAsync via GetObjectArgs + callback stream, ObjectNotFoundException → null - OpenReadAsync: same buffering, ObjectNotFoundException → FileNotFoundException - WriteAllBytesAsync via PutObjectArgs with MemoryStream + WithObjectSize - OpenWriteAsync returns DeferredWriteStream flushing through WriteAllBytesAsync - DeleteAsync: stat-then-remove, returns whether object existed - ListAsync: ListObjectsEnumAsync with WithRecursive(true), yields VfsEntry(Key, Size, LastModifiedDateTime) - CreateClient mirrors S3StorageService (URI parsing, WithSSL, optional WithRegion) - Dispose last, Interlocked-guarded - RegisterS3FileSystemExtensions: extension(IContainer) block using RegisterDelegate directly (no SquidStd.Vfs dep) - integration tests: MinioCollection fixture reused, 9 tests all green (Docker available locally) - slnx + test csproj updated to include new project --- SquidStd.slnx | 1 + .../Data/S3FileSystemOptions.cs | 13 + .../RegisterS3FileSystemExtensions.cs | 26 ++ src/SquidStd.Vfs.S3/Services/S3FileSystem.cs | 253 ++++++++++++++++++ src/SquidStd.Vfs.S3/SquidStd.Vfs.S3.csproj | 20 ++ tests/SquidStd.Tests/SquidStd.Tests.csproj | 1 + .../Vfs/S3/S3FileSystemTests.cs | 141 ++++++++++ 7 files changed, 455 insertions(+) create mode 100644 src/SquidStd.Vfs.S3/Data/S3FileSystemOptions.cs create mode 100644 src/SquidStd.Vfs.S3/Extensions/RegisterS3FileSystemExtensions.cs create mode 100644 src/SquidStd.Vfs.S3/Services/S3FileSystem.cs create mode 100644 src/SquidStd.Vfs.S3/SquidStd.Vfs.S3.csproj create mode 100644 tests/SquidStd.Tests/Vfs/S3/S3FileSystemTests.cs diff --git a/SquidStd.slnx b/SquidStd.slnx index b6978e23..768a0c35 100644 --- a/SquidStd.slnx +++ b/SquidStd.slnx @@ -40,6 +40,7 @@ + diff --git a/src/SquidStd.Vfs.S3/Data/S3FileSystemOptions.cs b/src/SquidStd.Vfs.S3/Data/S3FileSystemOptions.cs new file mode 100644 index 00000000..48adbf04 --- /dev/null +++ b/src/SquidStd.Vfs.S3/Data/S3FileSystemOptions.cs @@ -0,0 +1,13 @@ +using SquidStd.Aws.Abstractions.Data.Config; + +namespace SquidStd.Vfs.S3.Data; + +/// Connection options for the S3-compatible VFS backend (AWS / MinIO / R2 / B2). +public sealed class S3FileSystemOptions +{ + /// AWS-style connection config; ServiceUrl is the full endpoint for S3-compatibles. + public AwsConfigEntry Aws { get; init; } = new(); + + /// Bucket name. + public string Bucket { get; init; } = string.Empty; +} diff --git a/src/SquidStd.Vfs.S3/Extensions/RegisterS3FileSystemExtensions.cs b/src/SquidStd.Vfs.S3/Extensions/RegisterS3FileSystemExtensions.cs new file mode 100644 index 00000000..548a3df8 --- /dev/null +++ b/src/SquidStd.Vfs.S3/Extensions/RegisterS3FileSystemExtensions.cs @@ -0,0 +1,26 @@ +using DryIoc; +using SquidStd.Vfs.Abstractions.Interfaces; +using SquidStd.Vfs.S3.Data; +using SquidStd.Vfs.S3.Services; + +namespace SquidStd.Vfs.S3.Extensions; + +/// DryIoc registration helper for the S3-compatible VFS backend. +public static class RegisterS3FileSystemExtensions +{ + /// Container that receives the VFS registration. + extension(IContainer container) + { + /// Registers an backed by S3-compatible storage. + public IContainer RegisterS3FileSystem(Action configure) + { + ArgumentNullException.ThrowIfNull(configure); + + var options = new S3FileSystemOptions(); + configure(options); + container.RegisterDelegate(_ => new S3FileSystem(options), Reuse.Singleton); + + return container; + } + } +} diff --git a/src/SquidStd.Vfs.S3/Services/S3FileSystem.cs b/src/SquidStd.Vfs.S3/Services/S3FileSystem.cs new file mode 100644 index 00000000..b54f73a4 --- /dev/null +++ b/src/SquidStd.Vfs.S3/Services/S3FileSystem.cs @@ -0,0 +1,253 @@ +using System.Runtime.CompilerServices; +using Minio; +using Minio.DataModel.Args; +using Minio.Exceptions; +using SquidStd.Vfs.Abstractions; +using SquidStd.Vfs.Abstractions.Data; +using SquidStd.Vfs.Abstractions.Interfaces; +using SquidStd.Vfs.S3.Data; + +namespace SquidStd.Vfs.S3.Services; + +/// +/// S3-compatible backed by the MinIO client. +/// The bucket is created lazily on first use and the raw object bytes map directly to file contents. +/// +public sealed class S3FileSystem : IVirtualFileSystem, IDisposable +{ + private readonly string _bucket; + private readonly SemaphoreSlim _bucketLock = new(1, 1); + private readonly IMinioClient _client; + private bool _bucketReady; + private int _disposed; + + /// Initializes a new from . + /// Thrown when any required connection field is empty. + public S3FileSystem(S3FileSystemOptions options) + { + ArgumentNullException.ThrowIfNull(options); + ArgumentException.ThrowIfNullOrWhiteSpace(options.Aws.ServiceUrl); + ArgumentException.ThrowIfNullOrWhiteSpace(options.Aws.AccessKey); + ArgumentException.ThrowIfNullOrWhiteSpace(options.Aws.SecretKey); + ArgumentException.ThrowIfNullOrWhiteSpace(options.Bucket); + + _client = CreateClient(options); + _bucket = options.Bucket; + } + + /// + public async ValueTask ExistsAsync(string path, CancellationToken cancellationToken = default) + { + ArgumentException.ThrowIfNullOrWhiteSpace(path); + + await EnsureBucketAsync(cancellationToken); + + try + { + await _client.StatObjectAsync( + new StatObjectArgs().WithBucket(_bucket).WithObject(path), + cancellationToken + ); + + return true; + } + catch (ObjectNotFoundException) + { + return false; + } + } + + /// + public async ValueTask ReadAllBytesAsync(string path, CancellationToken cancellationToken = default) + { + ArgumentException.ThrowIfNullOrWhiteSpace(path); + + await EnsureBucketAsync(cancellationToken); + + using var buffer = new MemoryStream(); + + try + { + await _client.GetObjectAsync( + new GetObjectArgs() + .WithBucket(_bucket) + .WithObject(path) + .WithCallbackStream(async (stream, ct) => await stream.CopyToAsync(buffer, ct)), + cancellationToken + ); + } + catch (ObjectNotFoundException) + { + return null; + } + + return buffer.ToArray(); + } + + /// + public async ValueTask WriteAllBytesAsync(string path, ReadOnlyMemory data, CancellationToken cancellationToken = default) + { + ArgumentException.ThrowIfNullOrWhiteSpace(path); + + await EnsureBucketAsync(cancellationToken); + + var bytes = data.ToArray(); + using var stream = new MemoryStream(bytes, false); + + await _client.PutObjectAsync( + new PutObjectArgs() + .WithBucket(_bucket) + .WithObject(path) + .WithStreamData(stream) + .WithObjectSize(bytes.LongLength), + cancellationToken + ); + } + + /// + public async Task OpenReadAsync(string path, CancellationToken cancellationToken = default) + { + ArgumentException.ThrowIfNullOrWhiteSpace(path); + + await EnsureBucketAsync(cancellationToken); + + var buffer = new MemoryStream(); + + try + { + await _client.GetObjectAsync( + new GetObjectArgs() + .WithBucket(_bucket) + .WithObject(path) + .WithCallbackStream(async (stream, ct) => await stream.CopyToAsync(buffer, ct)), + cancellationToken + ); + } + catch (ObjectNotFoundException) + { + await buffer.DisposeAsync(); + throw new FileNotFoundException($"Object not found in S3 bucket '{_bucket}': '{path}'.", path); + } + + buffer.Position = 0; + return buffer; + } + + /// + public Task OpenWriteAsync(string path, CancellationToken cancellationToken = default) + { + ArgumentException.ThrowIfNullOrWhiteSpace(path); + + return Task.FromResult( + new DeferredWriteStream((bytes, ct) => WriteAllBytesAsync(path, bytes, ct), cancellationToken) + ); + } + + /// + public async ValueTask DeleteAsync(string path, CancellationToken cancellationToken = default) + { + ArgumentException.ThrowIfNullOrWhiteSpace(path); + + if (!await ExistsAsync(path, cancellationToken)) + { + return false; + } + + await _client.RemoveObjectAsync( + new RemoveObjectArgs().WithBucket(_bucket).WithObject(path), + cancellationToken + ); + + return true; + } + + /// + public async IAsyncEnumerable ListAsync( + string? prefix = null, + [EnumeratorCancellation] CancellationToken cancellationToken = default + ) + { + await EnsureBucketAsync(cancellationToken); + + var args = new ListObjectsArgs().WithBucket(_bucket).WithRecursive(true); + + if (!string.IsNullOrEmpty(prefix)) + { + args = args.WithPrefix(prefix); + } + + await foreach (var item in _client.ListObjectsEnumAsync(args, cancellationToken)) + { + if (!item.IsDir) + { + yield return new VfsEntry( + item.Key, + (long)item.Size, + item.LastModifiedDateTime?.ToUniversalTime() ?? default + ); + } + } + } + + private static IMinioClient CreateClient(S3FileSystemOptions options) + { + var uri = new Uri(options.Aws.ServiceUrl!, UriKind.Absolute); + var endpoint = uri.IsDefaultPort ? uri.Host : $"{uri.Host}:{uri.Port}"; + + var minio = new MinioClient() + .WithEndpoint(endpoint) + .WithCredentials(options.Aws.AccessKey, options.Aws.SecretKey) + .WithSSL(string.Equals(uri.Scheme, "https", StringComparison.OrdinalIgnoreCase)); + + if (!string.IsNullOrWhiteSpace(options.Aws.Region)) + { + minio = minio.WithRegion(options.Aws.Region); + } + + return minio.Build(); + } + + private async ValueTask EnsureBucketAsync(CancellationToken cancellationToken) + { + await _bucketLock.WaitAsync(cancellationToken); + + try + { + if (_bucketReady) + { + return; + } + + var exists = await _client.BucketExistsAsync( + new BucketExistsArgs().WithBucket(_bucket), + cancellationToken + ); + + if (!exists) + { + await _client.MakeBucketAsync( + new MakeBucketArgs().WithBucket(_bucket), + cancellationToken + ); + } + + _bucketReady = true; + } + finally + { + _bucketLock.Release(); + } + } + + /// + public void Dispose() + { + if (Interlocked.Exchange(ref _disposed, 1) != 0) + { + return; + } + + _client.Dispose(); + _bucketLock.Dispose(); + } +} diff --git a/src/SquidStd.Vfs.S3/SquidStd.Vfs.S3.csproj b/src/SquidStd.Vfs.S3/SquidStd.Vfs.S3.csproj new file mode 100644 index 00000000..3074c768 --- /dev/null +++ b/src/SquidStd.Vfs.S3/SquidStd.Vfs.S3.csproj @@ -0,0 +1,20 @@ + + + + net10.0 + enable + enable + true + + + + + + + + + + + + + diff --git a/tests/SquidStd.Tests/SquidStd.Tests.csproj b/tests/SquidStd.Tests/SquidStd.Tests.csproj index 69fab085..b1528560 100644 --- a/tests/SquidStd.Tests/SquidStd.Tests.csproj +++ b/tests/SquidStd.Tests/SquidStd.Tests.csproj @@ -73,6 +73,7 @@ + diff --git a/tests/SquidStd.Tests/Vfs/S3/S3FileSystemTests.cs b/tests/SquidStd.Tests/Vfs/S3/S3FileSystemTests.cs new file mode 100644 index 00000000..6695fef0 --- /dev/null +++ b/tests/SquidStd.Tests/Vfs/S3/S3FileSystemTests.cs @@ -0,0 +1,141 @@ +using System.Text; +using SquidStd.Tests.Storage.S3; +using SquidStd.Vfs.S3.Data; +using SquidStd.Vfs.S3.Services; + +namespace SquidStd.Tests.Vfs.S3; + +[Collection(MinioCollection.Name)] +public class S3FileSystemTests +{ + private readonly MinioContainerFixture _fixture; + + public S3FileSystemTests(MinioContainerFixture fixture) + { + _fixture = fixture; + } + + [Fact] + public void Ctor_MissingServiceUrl_Throws() + => Assert.ThrowsAny( + () => new S3FileSystem(new() { Aws = new() { AccessKey = "a", SecretKey = "b" }, Bucket = "c" }) + ); + + [Fact] + public async Task WriteAllBytesAsync_Then_ReadAllBytesAsync_RoundTrips() + { + using var fs = NewFileSystem(); + var path = Path(); + var data = Encoding.UTF8.GetBytes("hello s3 vfs"); + + await fs.WriteAllBytesAsync(path, data); + var read = await fs.ReadAllBytesAsync(path); + + Assert.NotNull(read); + Assert.Equal(data, read); + } + + [Fact] + public async Task OpenWriteAsync_Then_OpenReadAsync_RoundTrips() + { + using var fs = NewFileSystem(); + var path = Path(); + var data = Encoding.UTF8.GetBytes("deferred write stream"); + + await using (var write = await fs.OpenWriteAsync(path)) + { + await write.WriteAsync(data); + } + + await using var read = await fs.OpenReadAsync(path); + using var buffer = new MemoryStream(); + await read.CopyToAsync(buffer); + + Assert.Equal(data, buffer.ToArray()); + } + + [Fact] + public async Task ExistsAsync_ReturnsTrueAfterWrite_AndFalseForMissing() + { + using var fs = NewFileSystem(); + var path = Path(); + + Assert.False(await fs.ExistsAsync(path)); + + await fs.WriteAllBytesAsync(path, Encoding.UTF8.GetBytes("x")); + + Assert.True(await fs.ExistsAsync(path)); + } + + [Fact] + public async Task DeleteAsync_ExistingObject_ReturnsTrueAndRemoves() + { + using var fs = NewFileSystem(); + var path = Path(); + + await fs.WriteAllBytesAsync(path, Encoding.UTF8.GetBytes("delete me")); + + Assert.True(await fs.DeleteAsync(path)); + Assert.False(await fs.ExistsAsync(path)); + } + + [Fact] + public async Task DeleteAsync_MissingObject_ReturnsFalse() + { + using var fs = NewFileSystem(); + + Assert.False(await fs.DeleteAsync(Path())); + } + + [Fact] + public async Task ReadAllBytesAsync_MissingObject_ReturnsNull() + { + using var fs = NewFileSystem(); + + Assert.Null(await fs.ReadAllBytesAsync(Path())); + } + + [Fact] + public async Task OpenReadAsync_MissingObject_ThrowsFileNotFoundException() + { + using var fs = NewFileSystem(); + + await Assert.ThrowsAsync(() => fs.OpenReadAsync(Path())); + } + + [Fact] + public async Task ListAsync_Prefix_ReturnsWrittenKeys() + { + using var fs = NewFileSystem(); + var prefix = "list-" + Guid.NewGuid().ToString("N") + "/"; + + await fs.WriteAllBytesAsync(prefix + "a.txt", Encoding.UTF8.GetBytes("1")); + await fs.WriteAllBytesAsync(prefix + "b.txt", Encoding.UTF8.GetBytes("2")); + + var entries = new List(); + + await foreach (var entry in fs.ListAsync(prefix)) + { + entries.Add(entry.Path); + } + + Assert.Equal(2, entries.Count); + Assert.Contains(prefix + "a.txt", entries); + Assert.Contains(prefix + "b.txt", entries); + } + + private static string Path() + => "vfs-" + Guid.NewGuid().ToString("N"); + + private S3FileSystem NewFileSystem() + => new(new S3FileSystemOptions + { + Aws = new() + { + ServiceUrl = _fixture.ServiceUrl, + AccessKey = _fixture.AccessKey, + SecretKey = _fixture.SecretKey + }, + Bucket = "vfs-tests-" + Guid.NewGuid().ToString("N")[..8] + }); +} From dc9808e3a9f66dd47ef6bfde905d18c2843210a9 Mon Sep 17 00:00:00 2001 From: Tom Date: Tue, 30 Jun 2026 16:00:49 +0200 Subject: [PATCH 37/39] feat(vfs): add database filesystem backend (SquidStd.Vfs.Database) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - New project SquidStd.Vfs.Database targeting net10.0 with FreeSql 3.5.310 - VfsFileEntity: BaseEntity subclass with Path/Content/Size; unique index on Path via FreeSql class-level [Index("uq_vfs_file_path", "Path", true)] - DatabaseFileSystem: IVirtualFileSystem backed by IDataAccess; query-then-write upsert in WriteAllBytesAsync; StartsWith predicate for ListAsync prefix filtering; DeferredWriteStream for OpenWriteAsync - RegisterDatabaseFileSystemExtensions: extension(IContainer) DryIoc helper - Integration tests (SQLite, no Docker): write-read round-trip, upsert same-path deduplication, ExistsAsync, DeleteAsync, ListAsync with/without prefix — all 7 pass - Added SquidStd.Vfs.Database project reference to SquidStd.Tests --- .../Data/Entities/VfsFileEntity.cs | 18 +++ .../RegisterDatabaseFileSystemExtensions.cs | 29 ++++ .../Services/DatabaseFileSystem.cs | 120 ++++++++++++++++ .../SquidStd.Vfs.Database.csproj | 20 +++ tests/SquidStd.Tests/SquidStd.Tests.csproj | 1 + .../Vfs/Database/DatabaseFileSystemTests.cs | 130 ++++++++++++++++++ 6 files changed, 318 insertions(+) create mode 100644 src/SquidStd.Vfs.Database/Data/Entities/VfsFileEntity.cs create mode 100644 src/SquidStd.Vfs.Database/Extensions/RegisterDatabaseFileSystemExtensions.cs create mode 100644 src/SquidStd.Vfs.Database/Services/DatabaseFileSystem.cs create mode 100644 src/SquidStd.Vfs.Database/SquidStd.Vfs.Database.csproj create mode 100644 tests/SquidStd.Tests/Vfs/Database/DatabaseFileSystemTests.cs diff --git a/src/SquidStd.Vfs.Database/Data/Entities/VfsFileEntity.cs b/src/SquidStd.Vfs.Database/Data/Entities/VfsFileEntity.cs new file mode 100644 index 00000000..fff35a93 --- /dev/null +++ b/src/SquidStd.Vfs.Database/Data/Entities/VfsFileEntity.cs @@ -0,0 +1,18 @@ +using FreeSql.DataAnnotations; +using SquidStd.Database.Abstractions.Data.Entities; + +namespace SquidStd.Vfs.Database.Data.Entities; + +/// A stored file keyed by its logical VFS path. +[Index("uq_vfs_file_path", "Path", true)] +public sealed class VfsFileEntity : BaseEntity +{ + /// The logical VFS path (unique). + public string Path { get; set; } = string.Empty; + + /// The file bytes. + public byte[] Content { get; set; } = []; + + /// The file size in bytes. + public long Size { get; set; } +} diff --git a/src/SquidStd.Vfs.Database/Extensions/RegisterDatabaseFileSystemExtensions.cs b/src/SquidStd.Vfs.Database/Extensions/RegisterDatabaseFileSystemExtensions.cs new file mode 100644 index 00000000..c1b1b18b --- /dev/null +++ b/src/SquidStd.Vfs.Database/Extensions/RegisterDatabaseFileSystemExtensions.cs @@ -0,0 +1,29 @@ +using DryIoc; +using SquidStd.Database.Abstractions.Interfaces.Data; +using SquidStd.Vfs.Abstractions.Interfaces; +using SquidStd.Vfs.Database.Data.Entities; +using SquidStd.Vfs.Database.Services; + +namespace SquidStd.Vfs.Database.Extensions; + +/// DryIoc registration helper for the database VFS backend. +public static class RegisterDatabaseFileSystemExtensions +{ + /// Container that receives the VFS registration. + extension(IContainer container) + { + /// + /// Registers an backed by the database. + /// Requires the SquidStd.Database module to be registered (it supplies IDataAccess<>). + /// + public IContainer RegisterDatabaseFileSystem() + { + container.RegisterDelegate( + r => new DatabaseFileSystem(r.Resolve>()), + Reuse.Singleton + ); + + return container; + } + } +} diff --git a/src/SquidStd.Vfs.Database/Services/DatabaseFileSystem.cs b/src/SquidStd.Vfs.Database/Services/DatabaseFileSystem.cs new file mode 100644 index 00000000..02e92655 --- /dev/null +++ b/src/SquidStd.Vfs.Database/Services/DatabaseFileSystem.cs @@ -0,0 +1,120 @@ +using System.Runtime.CompilerServices; +using SquidStd.Database.Abstractions.Interfaces.Data; +using SquidStd.Vfs.Abstractions; +using SquidStd.Vfs.Abstractions.Data; +using SquidStd.Vfs.Abstractions.Interfaces; +using SquidStd.Vfs.Database.Data.Entities; + +namespace SquidStd.Vfs.Database.Services; + +/// +/// A implementation that stores files as rows in a relational +/// database via the generic abstraction (FreeSql). +/// +public sealed class DatabaseFileSystem : IVirtualFileSystem +{ + private readonly IDataAccess _data; + + /// + /// Initializes the database filesystem. + /// + /// The data access for rows. + public DatabaseFileSystem(IDataAccess dataAccess) + { + _data = dataAccess; + } + + /// + public async ValueTask ExistsAsync(string path, CancellationToken cancellationToken = default) + { + ArgumentException.ThrowIfNullOrEmpty(path); + + return await _data.ExistsAsync(e => e.Path == path, cancellationToken).ConfigureAwait(false); + } + + /// + public async ValueTask ReadAllBytesAsync(string path, CancellationToken cancellationToken = default) + { + ArgumentException.ThrowIfNullOrEmpty(path); + + var rows = await _data.QueryAsync(e => e.Path == path, cancellationToken).ConfigureAwait(false); + + return rows.Count > 0 ? rows[0].Content : null; + } + + /// + public async ValueTask WriteAllBytesAsync( + string path, + ReadOnlyMemory data, + CancellationToken cancellationToken = default + ) + { + ArgumentException.ThrowIfNullOrEmpty(path); + + var rows = await _data.QueryAsync(e => e.Path == path, cancellationToken).ConfigureAwait(false); + var existing = rows.Count > 0 ? rows[0] : null; + var bytes = data.ToArray(); + + if (existing is not null) + { + existing.Content = bytes; + existing.Size = bytes.Length; + await _data.UpdateAsync(existing, cancellationToken).ConfigureAwait(false); + } + else + { + await _data.InsertAsync( + new VfsFileEntity { Path = path, Content = bytes, Size = bytes.Length }, + cancellationToken + ).ConfigureAwait(false); + } + } + + /// + public async Task OpenReadAsync(string path, CancellationToken cancellationToken = default) + { + ArgumentException.ThrowIfNullOrEmpty(path); + + var bytes = await ReadAllBytesAsync(path, cancellationToken).ConfigureAwait(false) + ?? throw new FileNotFoundException($"No file at '{path}'.", path); + + return new MemoryStream(bytes, writable: false); + } + + /// + public Task OpenWriteAsync(string path, CancellationToken cancellationToken = default) + { + ArgumentException.ThrowIfNullOrEmpty(path); + + return Task.FromResult( + new DeferredWriteStream( + (bytes, ct) => WriteAllBytesAsync(path, bytes, ct), + cancellationToken + ) + ); + } + + /// + public async ValueTask DeleteAsync(string path, CancellationToken cancellationToken = default) + { + ArgumentException.ThrowIfNullOrEmpty(path); + + return await _data.BulkDeleteAsync(e => e.Path == path, cancellationToken).ConfigureAwait(false) > 0; + } + + /// + public async IAsyncEnumerable ListAsync( + string? prefix = null, + [EnumeratorCancellation] CancellationToken cancellationToken = default + ) + { + var rows = string.IsNullOrEmpty(prefix) + ? await _data.QueryAsync(cancellationToken: cancellationToken).ConfigureAwait(false) + : await _data.QueryAsync(e => e.Path.StartsWith(prefix), cancellationToken).ConfigureAwait(false); + + foreach (var e in rows) + { + yield return new VfsEntry(e.Path, e.Size, e.Updated); + } + } +} diff --git a/src/SquidStd.Vfs.Database/SquidStd.Vfs.Database.csproj b/src/SquidStd.Vfs.Database/SquidStd.Vfs.Database.csproj new file mode 100644 index 00000000..b267a428 --- /dev/null +++ b/src/SquidStd.Vfs.Database/SquidStd.Vfs.Database.csproj @@ -0,0 +1,20 @@ + + + + true + net10.0 + enable + enable + + + + + + + + + + + + + diff --git a/tests/SquidStd.Tests/SquidStd.Tests.csproj b/tests/SquidStd.Tests/SquidStd.Tests.csproj index b1528560..4c955ddc 100644 --- a/tests/SquidStd.Tests/SquidStd.Tests.csproj +++ b/tests/SquidStd.Tests/SquidStd.Tests.csproj @@ -74,6 +74,7 @@ + diff --git a/tests/SquidStd.Tests/Vfs/Database/DatabaseFileSystemTests.cs b/tests/SquidStd.Tests/Vfs/Database/DatabaseFileSystemTests.cs new file mode 100644 index 00000000..993ca6c4 --- /dev/null +++ b/tests/SquidStd.Tests/Vfs/Database/DatabaseFileSystemTests.cs @@ -0,0 +1,130 @@ +using System.Text; +using SquidStd.Database.Abstractions.Data.Database; +using SquidStd.Database.Data; +using SquidStd.Database.Services; +using SquidStd.Vfs.Database.Data.Entities; +using SquidStd.Vfs.Database.Services; + +namespace SquidStd.Tests.Vfs.Database; + +public sealed class DatabaseFileSystemTests : IAsyncLifetime +{ + private string _dbPath = string.Empty; + private DatabaseService _service = null!; + private DatabaseFileSystem _fs = null!; + + public async Task InitializeAsync() + { + _dbPath = Path.Combine(Path.GetTempPath(), "squidstd-vfsdb-" + Guid.NewGuid().ToString("N") + ".db"); + _service = new( + new DatabaseConfig + { + ConnectionString = $"sqlite://{_dbPath}", + AutoMigrate = true + } + ); + await _service.StartAsync(); + _fs = new DatabaseFileSystem(NewAccess()); + } + + public async Task DisposeAsync() + { + await _service.StopAsync(); + + if (File.Exists(_dbPath)) + { + File.Delete(_dbPath); + } + } + + [Fact] + public async Task WriteRead_RoundTrip() + { + var data = Encoding.UTF8.GetBytes("hello database"); + await _fs.WriteAllBytesAsync("docs/hello.txt", data); + + var result = await _fs.ReadAllBytesAsync("docs/hello.txt"); + + Assert.NotNull(result); + Assert.Equal("hello database", Encoding.UTF8.GetString(result)); + } + + [Fact] + public async Task WriteAsync_SamePath_Twice_ExactlyOneRow_LatestContent() + { + var access = NewAccess(); + var fs = new DatabaseFileSystem(access); + + await fs.WriteAllBytesAsync("file.txt", Encoding.UTF8.GetBytes("first")); + await fs.WriteAllBytesAsync("file.txt", Encoding.UTF8.GetBytes("second")); + + // Verify only one row exists and it has the latest content. + var rows = await access.QueryAsync(e => e.Path == "file.txt"); + Assert.Single(rows); + Assert.Equal("second", Encoding.UTF8.GetString(rows[0].Content)); + } + + [Fact] + public async Task ExistsAsync_TrueForWritten_FalseForMissing() + { + await _fs.WriteAllBytesAsync("present.bin", new byte[] { 0x01, 0x02 }); + + Assert.True(await _fs.ExistsAsync("present.bin")); + Assert.False(await _fs.ExistsAsync("absent.bin")); + } + + [Fact] + public async Task DeleteAsync_ExistingFile_ReturnsTrue_SubsequentReturnsFalse() + { + await _fs.WriteAllBytesAsync("todelete.txt", Encoding.UTF8.GetBytes("bye")); + + Assert.True(await _fs.DeleteAsync("todelete.txt")); + Assert.False(await _fs.DeleteAsync("todelete.txt")); + Assert.Null(await _fs.ReadAllBytesAsync("todelete.txt")); + } + + [Fact] + public async Task DeleteAsync_MissingFile_ReturnsFalse() + { + Assert.False(await _fs.DeleteAsync("nope.txt")); + } + + [Fact] + public async Task ListAsync_WithPrefix_ReturnsMatchingPaths() + { + await _fs.WriteAllBytesAsync("img/a.png", new byte[] { 1 }); + await _fs.WriteAllBytesAsync("img/b.png", new byte[] { 2 }); + await _fs.WriteAllBytesAsync("doc/c.txt", new byte[] { 3 }); + + var paths = new List(); + + await foreach (var entry in _fs.ListAsync("img")) + { + paths.Add(entry.Path); + } + + Assert.Contains("img/a.png", paths); + Assert.Contains("img/b.png", paths); + Assert.DoesNotContain("doc/c.txt", paths); + } + + [Fact] + public async Task ListAsync_NoPrefix_ReturnsAllPaths() + { + await _fs.WriteAllBytesAsync("x/1.dat", new byte[] { 10 }); + await _fs.WriteAllBytesAsync("y/2.dat", new byte[] { 20 }); + + var paths = new List(); + + await foreach (var entry in _fs.ListAsync()) + { + paths.Add(entry.Path); + } + + Assert.Contains("x/1.dat", paths); + Assert.Contains("y/2.dat", paths); + } + + private FreeSqlDataAccess NewAccess() + => new(_service); +} From 27f9b3073b6f36f4cd76932d63d30a19f7822962 Mon Sep 17 00:00:00 2001 From: Tom Date: Tue, 30 Jun 2026 16:05:48 +0200 Subject: [PATCH 38/39] docs(vfs): document VFS decorators and the S3/database backends --- SquidStd.slnx | 1 + src/SquidStd.Vfs.Database/README.md | 55 +++++++++++++++++++++++++++++ src/SquidStd.Vfs.S3/README.md | 55 +++++++++++++++++++++++++++++ src/SquidStd.Vfs/README.md | 26 ++++++++++++++ 4 files changed, 137 insertions(+) create mode 100644 src/SquidStd.Vfs.Database/README.md create mode 100644 src/SquidStd.Vfs.S3/README.md diff --git a/SquidStd.slnx b/SquidStd.slnx index 768a0c35..1905f0d3 100644 --- a/SquidStd.slnx +++ b/SquidStd.slnx @@ -41,6 +41,7 @@ + diff --git a/src/SquidStd.Vfs.Database/README.md b/src/SquidStd.Vfs.Database/README.md new file mode 100644 index 00000000..d50e938c --- /dev/null +++ b/src/SquidStd.Vfs.Database/README.md @@ -0,0 +1,55 @@ +

SquidStd.Vfs.Database

+ +An `IVirtualFileSystem` that stores files as rows in a relational database via SquidStd.Database / FreeSql. +Each file is a single `VfsFileEntity` row keyed by path; writes are upserts (last write wins, no concurrent +write safety guarantee). + +## Install + +```bash +dotnet add package SquidStd.Vfs.Database +``` + +## Usage + +Register `SquidStd.Database` first (it provides `IDataAccess<>`), then add the VFS backend: + +```csharp +using DryIoc; +using SquidStd.Vfs.Abstractions.Interfaces; +using SquidStd.Vfs.Database.Extensions; + +// SquidStd.Database must already be registered on the container. +container.RegisterDatabaseFileSystem(); + +var fs = container.Resolve(); + +await fs.WriteAllBytesAsync("config/settings.json", payload); +byte[]? data = await fs.ReadAllBytesAsync("config/settings.json"); + +await foreach (var entry in fs.ListAsync("config")) + Console.WriteLine($"{entry.Path} ({entry.Size} bytes)"); +``` + +The `VfsFileEntity` table is created automatically by FreeSql on first access (sync-structure mode). + +> **Single-writer assumption** — upsert operations are not serialised across concurrent writers. Use this +> backend in single-process scenarios or where last-write-wins is acceptable. + +## Key types + +| Type | Purpose | +|---|---| +| `RegisterDatabaseFileSystemExtensions` | `RegisterDatabaseFileSystem()` DryIoc registration. | +| `DatabaseFileSystem` | FreeSql-backed `IVirtualFileSystem`. | +| `VfsFileEntity` | ORM entity: `Path` (PK), `Content` (blob), `UpdatedAt`. | + +## Related + +- Tutorial: [Virtual filesystem](https://tgiachi.github.io/squid-std/tutorials/vfs.html) +- [`SquidStd.Vfs`](../SquidStd.Vfs/README.md) — core backends and decorators +- [`SquidStd.Database`](../SquidStd.Database/README.md) — required database module + +## License + +MIT — part of [SquidStd](https://github.com/tgiachi/squid-std). diff --git a/src/SquidStd.Vfs.S3/README.md b/src/SquidStd.Vfs.S3/README.md new file mode 100644 index 00000000..85ac32cf --- /dev/null +++ b/src/SquidStd.Vfs.S3/README.md @@ -0,0 +1,55 @@ +

SquidStd.Vfs.S3

+ +An `IVirtualFileSystem` over S3-compatible object storage — AWS S3, MinIO, Cloudflare R2, Backblaze B2 — via +the MinIO .NET SDK. Each logical path maps directly to an object key inside a single bucket. The bucket is +created lazily on first use. + +## Install + +```bash +dotnet add package SquidStd.Vfs.S3 +``` + +## Usage + +```csharp +using DryIoc; +using SquidStd.Vfs.Abstractions.Interfaces; +using SquidStd.Vfs.S3.Extensions; + +container.RegisterS3FileSystem(o => +{ + o.Bucket = "app-data"; + o.Aws.ServiceUrl = "https://s3.amazonaws.com"; // or MinIO/R2/B2 endpoint + o.Aws.AccessKey = "..."; + o.Aws.SecretKey = "..."; +}); + +var fs = container.Resolve(); + +await fs.WriteAllBytesAsync("reports/2026.json", payload); +byte[]? data = await fs.ReadAllBytesAsync("reports/2026.json"); + +await foreach (var entry in fs.ListAsync("reports")) + Console.WriteLine($"{entry.Path} ({entry.Size} bytes)"); +``` + +For native AWS with the default credential chain, omit `AccessKey`/`SecretKey` and set only the region via +`o.Aws.Region`. + +## Key types + +| Type | Purpose | +|---|---| +| `RegisterS3FileSystemExtensions` | `RegisterS3FileSystem(...)` DryIoc registration. | +| `S3FileSystem` | MinIO-backed `IVirtualFileSystem` with lazy bucket creation. | +| `S3FileSystemOptions` | `Bucket` + `Aws` (`ServiceUrl`, `AccessKey`, `SecretKey`, `Region`). | + +## Related + +- Tutorial: [Virtual filesystem](https://tgiachi.github.io/squid-std/tutorials/vfs.html) +- [`SquidStd.Vfs`](../SquidStd.Vfs/README.md) — core backends and decorators + +## License + +MIT — part of [SquidStd](https://github.com/tgiachi/squid-std). diff --git a/src/SquidStd.Vfs/README.md b/src/SquidStd.Vfs/README.md index 82c5e888..02089b5a 100644 --- a/src/SquidStd.Vfs/README.md +++ b/src/SquidStd.Vfs/README.md @@ -60,6 +60,32 @@ Logical paths are normalized (forward slashes, root-relative) and reject `..` tr | `VfsDirectories` | Named directory layout (`DirectoriesConfig` analogue) over any backend. | | `RegisterVfsExtensions` | `RegisterVfs(...)` registration. | +## Decorators + +Decorators wrap any `IVirtualFileSystem` to add behaviour without touching the backend. Stack them in any order. + +| Decorator | Description | +|---|---| +| `ReadOnlyFileSystem(inner)` | Delegates all reads to `inner`; rejects every mutation with `InvalidOperationException`. | +| `ScopedFileSystem(inner, prefix)` | Roots `inner` at a path prefix (chroot-like). All paths are resolved relative to the scope; list results are returned relative to it too. | +| `OverlayFileSystem(base, overlay)` | Reads overlay-first then falls back to base. Writes and deletes go to the overlay only. List returns the union of both; overlay entries shadow base entries with the same path. | +| `CachingFileSystem(remote, cache)` | Read-through cache: reads prefer the remote and refresh the cache copy on success; on a transport failure they fall back to the (possibly stale) cache. Writes are write-through (remote then cache) and fail when the remote is unreachable. | + +Composition example — S3 with a local disk cache for resilience to an unstable connection: + +```csharp +// S3 with a local disk cache for resilience to an unstable connection. +var fs = new CachingFileSystem( + remote: s3FileSystem, + cache: new PhysicalFileSystem("/var/cache/app")); +``` + +Decorators are not registered via DI helpers; construct them explicitly and pass the result to `RegisterVfs(...)`: + +```csharp +container.RegisterVfs(_ => new ReadOnlyFileSystem(new PhysicalFileSystem("/var/lib/app/data"))); +``` + ## Related - Tutorial: [Virtual filesystem](https://tgiachi.github.io/squid-std/tutorials/vfs.html) From d53cec1ff234120c6fead96be6741a404790744b Mon Sep 17 00:00:00 2001 From: Tom Date: Tue, 30 Jun 2026 18:27:22 +0200 Subject: [PATCH 39/39] docs: document TUI, password-based encryption, and the S3/database VFS backends - README: add SquidStd.Tui row under new ### User interface section (after Workers) - README: update SquidStd.Vfs row to mention composable decorators (ReadOnlyFileSystem, ScopedFileSystem, OverlayFileSystem, CachingFileSystem) - README: add SquidStd.Vfs.S3 and SquidStd.Vfs.Database rows to Storage section - README: update SquidStd.Crypto row to mention PasswordCipher and Argon2id + AES-256-GCM - README: extend Overview bullet for Data & storage to mention new VFS backends/decorators; add new Terminal UI bullet for SquidStd.Tui - docs/articles: add tui.md, vfs-s3.md, vfs-database.md include articles - docs/articles/toc.yml: add SquidStd.Vfs.S3, SquidStd.Vfs.Database after SquidStd.Vfs; add SquidStd.Tui before SquidStd.Crypto - docs/tutorials/tui.md: new tutorial covering CounterViewModel, imperative TuiView, declarative TuiComposedView DSL, DI boot, and ViewModel-first navigation - docs/tutorials/vfs.md: add S3 and database backend sections; add CachingFileSystem, ReadOnlyFileSystem, ScopedFileSystem, and OverlayFileSystem decorator examples - docs/tutorials/crypto.md: add password-based encryption section (PasswordCipher.Encrypt/ Decrypt, EncryptString/DecryptString, PbkdfCost.Sensitive, PasswordDecryptionException) - docs/tutorials/toc.yml: add Terminal UI (MVVM) entry --- README.md | 17 ++++- docs/articles/toc.yml | 6 ++ docs/articles/tui.md | 1 + docs/articles/vfs-database.md | 1 + docs/articles/vfs-s3.md | 1 + docs/tutorials/crypto.md | 31 ++++++++ docs/tutorials/toc.yml | 2 + docs/tutorials/tui.md | 135 ++++++++++++++++++++++++++++++++++ docs/tutorials/vfs.md | 94 +++++++++++++++++++++++ 9 files changed, 285 insertions(+), 3 deletions(-) create mode 100644 docs/articles/tui.md create mode 100644 docs/articles/vfs-database.md create mode 100644 docs/articles/vfs-s3.md create mode 100644 docs/tutorials/tui.md diff --git a/README.md b/README.md index ff64646b..a665af5b 100644 --- a/README.md +++ b/README.md @@ -44,11 +44,14 @@ it bundles the foundations you reach for again and again behind small, well-defi mailboxes), job system, timer/cron scheduler, metrics and health checks. - **Messaging & caching** — in-memory, RabbitMQ and AWS SQS/SNS transports; in-memory and Redis caches. - **Data & storage** — FreeSql data access, binary persistence (snapshot + WAL), local / S3 / MinIO - object storage, and a virtual filesystem (physical / zip / in-memory + an encrypted vault). + object storage, and a virtual filesystem (physical / zip / in-memory + an encrypted vault, plus + S3-compatible and database-backed backends and composable decorators). - **Search, mail & workers** — Elasticsearch indexing with a constrained LINQ provider, IMAP/POP3 mail polling and an outbound mail queue, and a worker / manager runtime. - **Networking, scripting & observability** — TCP/UDP servers with a framing/middleware pipeline, Lua scripting and Scriban templating, and OpenTelemetry tracing + metrics export. +- **Terminal UI** — MVVM for terminal apps via `SquidStd.Tui`: observable ViewModels, a fluent + binder (+ `AutoBind` and a declarative DSL), ViewModel-first navigation, DryIoc wiring. Everything is modular: take only the packages you need, each behind a clean abstraction with an in-memory implementation for tests and an external backend for production. @@ -195,13 +198,15 @@ dotnet new squidstd-manager -n Acme.Manager --messaging inmemory | `SquidStd.Storage` | Local file storage backend (`AddFileStorage`). | [![readme](https://img.shields.io/badge/readme-1390A3.svg)](src/SquidStd.Storage/README.md) · [![NuGet](https://img.shields.io/nuget/v/SquidStd.Storage.svg)](https://www.nuget.org/packages/SquidStd.Storage/) | | `SquidStd.Storage.S3` | S3/MinIO storage backend (`AddS3Storage`). | [![readme](https://img.shields.io/badge/readme-1390A3.svg)](src/SquidStd.Storage.S3/README.md) · [![NuGet](https://img.shields.io/nuget/v/SquidStd.Storage.S3.svg)](https://www.nuget.org/packages/SquidStd.Storage.S3/) | | `SquidStd.Vfs.Abstractions` | Virtual filesystem contracts (`IVirtualFileSystem`, `ILockableFileSystem`, `VfsPath`). | [![readme](https://img.shields.io/badge/readme-1390A3.svg)](src/SquidStd.Vfs.Abstractions/README.md) · [![NuGet](https://img.shields.io/nuget/v/SquidStd.Vfs.Abstractions.svg)](https://www.nuget.org/packages/SquidStd.Vfs.Abstractions/) | -| `SquidStd.Vfs` | Virtual filesystem providers — physical, in-memory, and zip — plus `VfsDirectories`, a VFS-backed `DirectoriesConfig`. | [![readme](https://img.shields.io/badge/readme-1390A3.svg)](src/SquidStd.Vfs/README.md) · [![NuGet](https://img.shields.io/nuget/v/SquidStd.Vfs.svg)](https://www.nuget.org/packages/SquidStd.Vfs/) | +| `SquidStd.Vfs` | Virtual filesystem providers — physical, in-memory, and zip — plus composable decorators (`ReadOnlyFileSystem`, `ScopedFileSystem`, `OverlayFileSystem`, `CachingFileSystem`) and `VfsDirectories`. | [![readme](https://img.shields.io/badge/readme-1390A3.svg)](src/SquidStd.Vfs/README.md) · [![NuGet](https://img.shields.io/nuget/v/SquidStd.Vfs.svg)](https://www.nuget.org/packages/SquidStd.Vfs/) | +| `SquidStd.Vfs.S3` | S3-compatible VFS backend — AWS S3, MinIO, Cloudflare R2, Backblaze B2 — via the MinIO SDK (`RegisterS3FileSystem`). | [![readme](https://img.shields.io/badge/readme-1390A3.svg)](src/SquidStd.Vfs.S3/README.md) · [![NuGet](https://img.shields.io/nuget/v/SquidStd.Vfs.S3.svg)](https://www.nuget.org/packages/SquidStd.Vfs.S3/) | +| `SquidStd.Vfs.Database` | Database-backed VFS storing files as rows via SquidStd.Database / FreeSql (`RegisterDatabaseFileSystem`). | [![readme](https://img.shields.io/badge/readme-1390A3.svg)](src/SquidStd.Vfs.Database/README.md) · [![NuGet](https://img.shields.io/nuget/v/SquidStd.Vfs.Database.svg)](https://www.nuget.org/packages/SquidStd.Vfs.Database/) | ### Security — crypto & secrets | Package | Description | Links | |---------|-------------|-------| -| `SquidStd.Crypto` | OpenPGP key management/operations over an indexed keyring (`SquidStd.Crypto.Pgp`, `RegisterPgp`) plus the encrypted VFS vault decorator (Argon2id + per-entry AES-GCM). | [![readme](https://img.shields.io/badge/readme-1390A3.svg)](src/SquidStd.Crypto/README.md) · [![NuGet](https://img.shields.io/nuget/v/SquidStd.Crypto.svg)](https://www.nuget.org/packages/SquidStd.Crypto/) | +| `SquidStd.Crypto` | OpenPGP key management/operations over an indexed keyring (`SquidStd.Crypto.Pgp`, `RegisterPgp`), password-based encryption (`PasswordCipher`, Argon2id + AES-256-GCM with a self-describing envelope), and the encrypted VFS vault decorator. | [![readme](https://img.shields.io/badge/readme-1390A3.svg)](src/SquidStd.Crypto/README.md) · [![NuGet](https://img.shields.io/nuget/v/SquidStd.Crypto.svg)](https://www.nuget.org/packages/SquidStd.Crypto/) | | `SquidStd.Secrets.Aws` | AWS adapters for the secret seams — KMS envelope `ISecretProtector` and Secrets Manager `ISecretStore`. | [![readme](https://img.shields.io/badge/readme-1390A3.svg)](src/SquidStd.Secrets.Aws/README.md) · [![NuGet](https://img.shields.io/nuget/v/SquidStd.Secrets.Aws.svg)](https://www.nuget.org/packages/SquidStd.Secrets.Aws/) | ### Search @@ -227,6 +232,12 @@ dotnet new squidstd-manager -n Acme.Manager --messaging inmemory | `SquidStd.Workers` | Worker runtime: consume jobs, dispatch to `IJobHandler`s, publish heartbeats (`AddWorkers`). | [![readme](https://img.shields.io/badge/readme-1390A3.svg)](src/SquidStd.Workers/README.md) · [![NuGet](https://img.shields.io/nuget/v/SquidStd.Workers.svg)](https://www.nuget.org/packages/SquidStd.Workers/) | | `SquidStd.Workers.Manager` | Job enqueue, heartbeat registry, offline sweep, opt-in ASP.NET endpoints (`AddWorkerManager`). | [![readme](https://img.shields.io/badge/readme-1390A3.svg)](src/SquidStd.Workers.Manager/README.md) · [![NuGet](https://img.shields.io/nuget/v/SquidStd.Workers.Manager.svg)](https://www.nuget.org/packages/SquidStd.Workers.Manager/) | +### User interface + +| Package | Description | Links | +|---------|-------------|-------| +| `SquidStd.Tui` | MVVM for terminal apps on Terminal.Gui v2 — observable ViewModels, a fluent binder (+ `AutoBind` and a declarative DSL), ViewModel-first navigation, DryIoc wiring. | [![readme](https://img.shields.io/badge/readme-1390A3.svg)](src/SquidStd.Tui/README.md) · [![NuGet](https://img.shields.io/nuget/v/SquidStd.Tui.svg)](https://www.nuget.org/packages/SquidStd.Tui/) | + ### Scripting & templating | Package | Description | Links | diff --git a/docs/articles/toc.yml b/docs/articles/toc.yml index cc2d4e4e..144f2b18 100644 --- a/docs/articles/toc.yml +++ b/docs/articles/toc.yml @@ -80,6 +80,10 @@ href: vfs-abstractions.md - name: SquidStd.Vfs href: vfs.md +- name: SquidStd.Vfs.S3 + href: vfs-s3.md +- name: SquidStd.Vfs.Database + href: vfs-database.md - name: SquidStd.Scripting.Lua href: scripting-lua.md - name: SquidStd.Templating @@ -114,6 +118,8 @@ href: telemetry-opentelemetry.md - name: SquidStd.Aws.Abstractions href: aws-abstractions.md +- name: SquidStd.Tui + href: tui.md - name: SquidStd.Crypto href: crypto.md - name: SquidStd.Secrets.Aws diff --git a/docs/articles/tui.md b/docs/articles/tui.md new file mode 100644 index 00000000..9cb161da --- /dev/null +++ b/docs/articles/tui.md @@ -0,0 +1 @@ +[!include[](../../src/SquidStd.Tui/README.md)] diff --git a/docs/articles/vfs-database.md b/docs/articles/vfs-database.md new file mode 100644 index 00000000..d91f5b38 --- /dev/null +++ b/docs/articles/vfs-database.md @@ -0,0 +1 @@ +[!include[](../../src/SquidStd.Vfs.Database/README.md)] diff --git a/docs/articles/vfs-s3.md b/docs/articles/vfs-s3.md new file mode 100644 index 00000000..35b71081 --- /dev/null +++ b/docs/articles/vfs-s3.md @@ -0,0 +1 @@ +[!include[](../../src/SquidStd.Vfs.S3/README.md)] diff --git a/docs/tutorials/crypto.md b/docs/tutorials/crypto.md index 95070492..6e8f9b65 100644 --- a/docs/tutorials/crypto.md +++ b/docs/tutorials/crypto.md @@ -43,6 +43,37 @@ survives a restart. [!code-csharp[](../../samples/SquidStd.Samples.Crypto/Program.cs#step-4)] +## Password-based encryption + +`PasswordCipher` encrypts a payload directly under a password — no key management required. Argon2id +derives the key, AES-256-GCM seals the data, and the result is a self-describing, versioned envelope +(salt, nonce, tag and KDF cost are embedded). Decryption needs only the password and the blob. + +```csharp +using SquidStd.Crypto.Password; +using SquidStd.Crypto.Password.Data; + +// Bytes round-trip. +byte[] blob = PasswordCipher.Encrypt(payloadBytes, "correct horse battery staple"); +byte[] back = PasswordCipher.Decrypt(blob, "correct horse battery staple"); + +// Text round-trip — the envelope is base64-encoded, safe to store in config or JSON. +string protectedText = PasswordCipher.EncryptString("a secret", "pw"); +string clear = PasswordCipher.DecryptString(protectedText, "pw"); +``` + +The cost of the Argon2id key derivation defaults to `PbkdfCost.Moderate`. Raise it when protecting +long-lived secrets: + +```csharp +// PbkdfCost.Sensitive — slower derivation, stronger resistance to offline attacks. +byte[] strong = PasswordCipher.Encrypt(payloadBytes, "pw", PbkdfCost.Sensitive); +``` + +A wrong password or tampered data raises `PasswordDecryptionException`. Use `PasswordCipher` for +user-supplied passwords; for app-key or KMS encryption use `CryptoUtils` / `ISecretProtector` +instead. + ## Run it ```bash diff --git a/docs/tutorials/toc.yml b/docs/tutorials/toc.yml index 2d88e934..aa2de778 100644 --- a/docs/tutorials/toc.yml +++ b/docs/tutorials/toc.yml @@ -44,5 +44,7 @@ href: vfs.md - name: Crypto (PGP) href: crypto.md +- name: Terminal UI (MVVM) + href: tui.md - name: Secrets (KMS / Secrets Manager) href: secrets.md diff --git a/docs/tutorials/tui.md b/docs/tutorials/tui.md new file mode 100644 index 00000000..cecafc80 --- /dev/null +++ b/docs/tutorials/tui.md @@ -0,0 +1,135 @@ +# Terminal UI (MVVM) + +Build a Counter terminal app with an observable ViewModel, then display it two ways — the +imperative `TuiView` and the declarative `TuiComposedView` DSL. + +## What you'll build + +A small Counter app where a `CounterViewModel` holds a `Value` property and an `IncrementCommand`. +You will see both ways to build the view: + +1. **Imperative** — `TuiView`: override `BuildLayout` to add Terminal.Gui widgets, then + override `Bind` to wire them through `ViewBinder`. +2. **Declarative** — `TuiComposedView`: override `Compose` and return a `TuiNode` tree built + with the `Ui.*` DSL; bindings and layout are inferred from the node types. + +## Prerequisites + +- .NET 10 SDK +- `dotnet add package SquidStd.Tui` + +## Steps + +### 1. Define the ViewModel + +Derive from `TuiViewModel` (which extends `ObservableObject` from CommunityToolkit.Mvvm) and mark +observable properties and relay commands with the standard source-generator attributes. + +```csharp +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using SquidStd.Tui.ViewModels; + +public sealed partial class CounterViewModel : TuiViewModel +{ + [ObservableProperty] + private string _title = "Counter"; + + [ObservableProperty] + private string _value = "0"; + + [RelayCommand] + private void Increment() => Value = (int.Parse(Value) + 1).ToString(); +} +``` + +### 2. Imperative view — `TuiView` + +Override `BuildLayout` to create and add Terminal.Gui widgets, then override `Bind` to wire them +through `ViewBinder`. The binder tracks property-changed notifications and updates widgets without +polling. + +```csharp +using Terminal.Gui; +using SquidStd.Tui.Views; +using SquidStd.Tui.Binders; + +public sealed class CounterView : TuiView +{ + private Label _value = null!; + private Button _inc = null!; + + protected override void BuildLayout() + { + _value = new Label { X = 1, Y = 1 }; + _inc = new Button { X = 1, Y = 3, Text = "+1" }; + Add(_value, _inc); + } + + protected override void Bind(ViewBinder b) + { + b.OneWayTitle(ViewModel, x => x.Title, this); + b.OneWayText(ViewModel, x => x.Value, _value); + b.Command(_inc, ViewModel.IncrementCommand); + } +} +``` + +### 3. Declarative view — `TuiComposedView` + +Instead of `BuildLayout` + `Bind`, derive from `TuiComposedView` and return a node tree from +`Compose`. The framework infers binding direction from the node type (Label → one-way, TextField → +two-way, Button → command) and applies layout automatically. + +```csharp +using SquidStd.Tui.Views; +using SquidStd.Tui.Dsl; + +public sealed class CounterView : TuiComposedView +{ + protected override TuiNode Compose() => + Ui.VStack( + Ui.Label(x => x.Title), + Ui.Label(x => x.Value), + Ui.Button("+1", x => x.IncrementCommand)); +} +``` + +`Ui.VStack` / `Ui.HStack` stack children vertically or horizontally and auto-assign `Y`/`X` +offsets. `Ui.TextField(x => x.Name)` is two-way by default; pass `BindMode.OneWay` to override. + +### 4. Register and run + +`RegisterTui()` adds the core TUI services and `TuiApplicationHost` to the DryIoc container. +`RegisterView()` registers the view for ViewModel-first resolution. +`TuiApplicationHost.RunAsync()` resolves the root ViewModel, shows its view, and starts +the Terminal.Gui event loop. + +```csharp +using DryIoc; +using SquidStd.Tui.Extensions; +using SquidStd.Tui.Hosts; + +var container = new Container().RegisterTui(); +container.RegisterView(); + +await container.Resolve().RunAsync(); +``` + +### 5. ViewModel-first navigation + +Push a new ViewModel onto the navigation stack from inside an existing ViewModel via the injected +`ITuiNavigator`: + +```csharp +// inside CounterViewModel +await Navigator.NavigateToAsync(); +await Navigator.BackAsync(); +``` + +The navigator resolves the view that was registered for `DetailViewModel` and shows it; `BackAsync` +pops the stack and restores the previous view. + +## Next steps + +- [SquidStd.Tui reference](../articles/tui.md) diff --git a/docs/tutorials/vfs.md b/docs/tutorials/vfs.md index 92082caa..769e599d 100644 --- a/docs/tutorials/vfs.md +++ b/docs/tutorials/vfs.md @@ -42,6 +42,98 @@ zip — as a lockable singleton. [!code-csharp[](../../samples/SquidStd.Samples.Vfs/Program.cs#step-3)] +## Alternative backends + +### S3-compatible storage + +Swap the in-memory backend for an S3-compatible store (AWS S3, MinIO, Cloudflare R2, Backblaze B2) +by installing `SquidStd.Vfs.S3` and replacing the `RegisterVfs` call: + +```bash +dotnet add package SquidStd.Vfs.S3 +``` + +```csharp +using SquidStd.Vfs.S3.Extensions; + +container.RegisterS3FileSystem(o => +{ + o.Bucket = "app-data"; + o.Aws.ServiceUrl = "https://s3.amazonaws.com"; // or MinIO/R2/B2 endpoint + o.Aws.AccessKey = "..."; + o.Aws.SecretKey = "..."; +}); + +// IVirtualFileSystem is now backed by S3 — the rest of your code is unchanged. +var fs = container.Resolve(); +await fs.WriteAllBytesAsync("reports/2026.json", payload); +``` + +For native AWS with the default credential chain, omit `AccessKey`/`SecretKey` and set only +`o.Aws.Region`. + +### Database-backed storage + +Store files as rows in a relational database (SQLite, MySQL, PostgreSQL, …) with +`SquidStd.Vfs.Database`. Register `SquidStd.Database` first, then add the VFS backend: + +```bash +dotnet add package SquidStd.Vfs.Database +``` + +```csharp +using SquidStd.Vfs.Database.Extensions; + +// SquidStd.Database must already be registered on the container. +container.RegisterDatabaseFileSystem(); + +var fs = container.Resolve(); +await fs.WriteAllBytesAsync("config/settings.json", payload); +``` + +FreeSql creates the `VfsFileEntity` table automatically on first access. This backend is +well-suited to single-process scenarios or where last-write-wins is acceptable. + +## Composable decorators + +Decorators wrap any `IVirtualFileSystem` to add behaviour without touching the backend. Construct +them directly and pass the result to `RegisterVfs`: + +### Offline-resilient reads with `CachingFileSystem` + +Wrap a remote backend (S3, database) with a local cache to keep reads working when the remote is +temporarily unreachable: + +```csharp +using SquidStd.Vfs.Services; +using SquidStd.Vfs.S3.Services; + +var s3 = new S3FileSystem(s3Options); + +container.RegisterVfs(_ => new CachingFileSystem( + remote: s3, + cache: new PhysicalFileSystem("/var/cache/app"))); +``` + +Reads prefer the remote and refresh the cache on success; on a transport failure they fall back to +the stale cached copy. Writes are write-through (remote then cache) and fail when the remote is +unreachable. + +### Other decorators + +```csharp +// Reject all writes — safe read-only access to a shared backend. +container.RegisterVfs(_ => new ReadOnlyFileSystem(new PhysicalFileSystem("/shared/data"))); + +// Chroot to a subdirectory — all paths are resolved relative to the prefix. +container.RegisterVfs(_ => new ScopedFileSystem(new PhysicalFileSystem("/var/lib/app"), "tenant-1")); + +// Overlay — reads overlay-first then fall back to base; writes go to the overlay only. +container.RegisterVfs(_ => new OverlayFileSystem( + baseFileSystem: new PhysicalFileSystem("/defaults"), + overlay: new InMemoryFileSystem())); +``` + ## Run it ```bash @@ -51,3 +143,5 @@ dotnet run --project samples/SquidStd.Samples.Vfs ## Next steps - [SquidStd.Vfs reference](../articles/vfs.md) +- [SquidStd.Vfs.S3 reference](../articles/vfs-s3.md) +- [SquidStd.Vfs.Database reference](../articles/vfs-database.md)