From 38b3b752c7b16c3b635225af5f6ee5dc52058488 Mon Sep 17 00:00:00 2001 From: Thorsten Thiel Date: Thu, 29 Aug 2024 19:29:12 +0200 Subject: [PATCH] Add caching to selectors --- src/.config/dotnet-tools.json | 13 + src/.gitignore | 4 +- .../Attributes/SelectorAttribute.cs | 0 .../Fluss.Regen.Tests.csproj | 30 - .../SampleIncrementalSourceGeneratorTests.cs | 77 --- .../Utils/Extensions/MethodBaseExtension.cs | 20 - .../Utils/Extensions/SnapshotExtensions.cs | 32 - .../Utils/Extensions/TypeExtensions.cs | 26 - .../Utils/Extensions/WriterExtensions.cs | 48 -- .../ExceptionSnapshotValueFormatter.cs | 16 - ...orDriverRunResultSnapshotValueFormatter.cs | 30 - .../HttpResponseSnapshotValueFormatter.cs | 41 -- .../IMarkdownSnapshotValueFormatter.cs | 32 - .../Formatters/ISnapshotValueFormatter.cs | 32 - .../Formatters/JsonSnapshotValueFormatter.cs | 64 -- .../PlainTextSnapshotValueFormatter.cs | 18 - .../Formatters/SnapshotValueFormatter.cs | 35 -- .../Utils/ISnapshotSegment.cs | 6 - .../Fluss.Regen.Tests/Utils/Snapshot.cs | 545 ------------------ .../Utils/TestAdditionalFile.cs | 14 - ...eneratorTests.GeneratesForAsyncSelector.md | 70 --- ...ratorTests.GeneratesForNonAsyncSelector.md | 70 --- ...eneratorTests.GeneratesForAsyncSelector.md | 83 --- ...ratorTests.GeneratesForNonAsyncSelector.md | 69 --- .../{Fluss.Regen => }/Fluss.Regen.csproj | 1 + .../Generators/ModuleSyntaxGenerator.cs | 198 ------- .../Generators/SelectorSyntaxGenerator.cs | 194 ------- .../SampleIncrementalSourceGenerator.cs | 125 ---- .../Generators/SelectorSyntaxGenerator.cs | 181 ++++++ .../Generators/StringBuilderPool.cs | 0 .../{Fluss.Regen => }/Helpers/CodeWriter.cs | 4 +- .../Helpers/CodeWriterExtensions.cs | 22 +- .../Helpers/SymbolExtensions.cs | 0 .../Inspectors/ISyntaxInfo.cs | 0 .../Inspectors/ISyntaxInspector.cs | 0 .../Inspectors/SelectorInfo.cs | 17 +- .../Inspectors/SelectorInspector.cs | 0 .../Inspectors/SyntaxInfoComparer.cs | 0 .../Properties/launchSettings.json | 0 src/Fluss.Regen/{Fluss.Regen => }/Readme.md | 0 ...oLoadGenerator.cs => SelectorGenerator.cs} | 100 ++-- src/Fluss.Sample/Fluss.Sample.csproj | 21 - src/Fluss.Sample/Program.cs | 19 - src/Fluss.Sample/Test.cs | 12 - .../ArbitraryUserUnitOfWorkExtensionTest.cs | 22 +- .../Core/Extensions/ValueTaskTest.cs | 8 +- .../Core/SideEffects/DispatcherTest.cs | 14 +- .../UnitOfWorkAndAuthorizationTest.cs | 2 +- .../Core/Validation/RootValidatorTests.cs | 14 +- src/Fluss.UnitTest/Fluss.UnitTest.csproj | 11 +- .../Regen/SelectorGeneratorTests.cs | 118 ++++ ...ncSelector#SelectorAttribute.g.verified.cs | 10 + ...esForAsyncSelector#Selectors.g.verified.cs | 82 +++ ...ncSelector#SelectorAttribute.g.verified.cs | 10 + ...orNonAsyncSelector#Selectors.g.verified.cs | 54 ++ ...rkSelector#SelectorAttribute.g.verified.cs | 10 + ...UnitOfWorkSelector#Selectors.g.verified.cs | 56 ++ src/Fluss.UnitTest/Setup.cs | 13 + src/Fluss.sln | 14 +- src/Fluss.sln.DotSettings.user | 5 +- src/Fluss/Events/EventListener.cs | 15 +- src/Fluss/UnitOfWork/IUnitOfWork.cs | 9 +- src/Fluss/UnitOfWork/IWriteUnitOfWork.cs | 14 + src/Fluss/UnitOfWork/UnitOfWork.ReadModels.cs | 35 ++ src/Fluss/UnitOfWork/UnitOfWork.cs | 2 +- src/Fluss/UnitOfWork/UnitOfWorkFactory.cs | 4 +- .../UnitOfWork/UnitOfWorkRecordingProxy.cs | 110 ++++ src/Fluss/Validation/RootValidator.cs | 1 - 68 files changed, 833 insertions(+), 2069 deletions(-) create mode 100644 src/.config/dotnet-tools.json rename src/Fluss.Regen/{Fluss.Regen => }/Attributes/SelectorAttribute.cs (100%) delete mode 100644 src/Fluss.Regen/Fluss.Regen.Tests/Fluss.Regen.Tests.csproj delete mode 100644 src/Fluss.Regen/Fluss.Regen.Tests/SampleIncrementalSourceGeneratorTests.cs delete mode 100644 src/Fluss.Regen/Fluss.Regen.Tests/Utils/Extensions/MethodBaseExtension.cs delete mode 100644 src/Fluss.Regen/Fluss.Regen.Tests/Utils/Extensions/SnapshotExtensions.cs delete mode 100644 src/Fluss.Regen/Fluss.Regen.Tests/Utils/Extensions/TypeExtensions.cs delete mode 100644 src/Fluss.Regen/Fluss.Regen.Tests/Utils/Extensions/WriterExtensions.cs delete mode 100644 src/Fluss.Regen/Fluss.Regen.Tests/Utils/Formatters/ExceptionSnapshotValueFormatter.cs delete mode 100644 src/Fluss.Regen/Fluss.Regen.Tests/Utils/Formatters/GeneratorDriverRunResultSnapshotValueFormatter.cs delete mode 100644 src/Fluss.Regen/Fluss.Regen.Tests/Utils/Formatters/HttpResponseSnapshotValueFormatter.cs delete mode 100644 src/Fluss.Regen/Fluss.Regen.Tests/Utils/Formatters/IMarkdownSnapshotValueFormatter.cs delete mode 100644 src/Fluss.Regen/Fluss.Regen.Tests/Utils/Formatters/ISnapshotValueFormatter.cs delete mode 100644 src/Fluss.Regen/Fluss.Regen.Tests/Utils/Formatters/JsonSnapshotValueFormatter.cs delete mode 100644 src/Fluss.Regen/Fluss.Regen.Tests/Utils/Formatters/PlainTextSnapshotValueFormatter.cs delete mode 100644 src/Fluss.Regen/Fluss.Regen.Tests/Utils/Formatters/SnapshotValueFormatter.cs delete mode 100644 src/Fluss.Regen/Fluss.Regen.Tests/Utils/ISnapshotSegment.cs delete mode 100644 src/Fluss.Regen/Fluss.Regen.Tests/Utils/Snapshot.cs delete mode 100644 src/Fluss.Regen/Fluss.Regen.Tests/Utils/TestAdditionalFile.cs delete mode 100644 src/Fluss.Regen/Fluss.Regen.Tests/__snapshots__/SampleIncrementalSourceGeneratorTests.GeneratesForAsyncSelector.md delete mode 100644 src/Fluss.Regen/Fluss.Regen.Tests/__snapshots__/SampleIncrementalSourceGeneratorTests.GeneratesForNonAsyncSelector.md delete mode 100644 src/Fluss.Regen/Fluss.Regen.Tests/__snapshots__/__MISMATCH__/SampleIncrementalSourceGeneratorTests.GeneratesForAsyncSelector.md delete mode 100644 src/Fluss.Regen/Fluss.Regen.Tests/__snapshots__/__MISMATCH__/SampleIncrementalSourceGeneratorTests.GeneratesForNonAsyncSelector.md rename src/Fluss.Regen/{Fluss.Regen => }/Fluss.Regen.csproj (91%) delete mode 100644 src/Fluss.Regen/Fluss.Regen/Generators/ModuleSyntaxGenerator.cs delete mode 100644 src/Fluss.Regen/Fluss.Regen/Generators/SelectorSyntaxGenerator.cs delete mode 100644 src/Fluss.Regen/Fluss.Regen/SampleIncrementalSourceGenerator.cs create mode 100644 src/Fluss.Regen/Generators/SelectorSyntaxGenerator.cs rename src/Fluss.Regen/{Fluss.Regen => }/Generators/StringBuilderPool.cs (100%) rename src/Fluss.Regen/{Fluss.Regen => }/Helpers/CodeWriter.cs (99%) rename src/Fluss.Regen/{Fluss.Regen => }/Helpers/CodeWriterExtensions.cs (51%) rename src/Fluss.Regen/{Fluss.Regen => }/Helpers/SymbolExtensions.cs (100%) rename src/Fluss.Regen/{Fluss.Regen => }/Inspectors/ISyntaxInfo.cs (100%) rename src/Fluss.Regen/{Fluss.Regen => }/Inspectors/ISyntaxInspector.cs (100%) rename src/Fluss.Regen/{Fluss.Regen => }/Inspectors/SelectorInfo.cs (79%) rename src/Fluss.Regen/{Fluss.Regen => }/Inspectors/SelectorInspector.cs (100%) rename src/Fluss.Regen/{Fluss.Regen => }/Inspectors/SyntaxInfoComparer.cs (100%) rename src/Fluss.Regen/{Fluss.Regen => }/Properties/launchSettings.json (100%) rename src/Fluss.Regen/{Fluss.Regen => }/Readme.md (100%) rename src/Fluss.Regen/{Fluss.Regen/AutoLoadGenerator.cs => SelectorGenerator.cs} (65%) delete mode 100644 src/Fluss.Sample/Fluss.Sample.csproj delete mode 100644 src/Fluss.Sample/Program.cs delete mode 100644 src/Fluss.Sample/Test.cs create mode 100644 src/Fluss.UnitTest/Regen/SelectorGeneratorTests.cs create mode 100644 src/Fluss.UnitTest/Regen/Snapshots/SelectorGeneratorTests.GeneratesForAsyncSelector#SelectorAttribute.g.verified.cs create mode 100644 src/Fluss.UnitTest/Regen/Snapshots/SelectorGeneratorTests.GeneratesForAsyncSelector#Selectors.g.verified.cs create mode 100644 src/Fluss.UnitTest/Regen/Snapshots/SelectorGeneratorTests.GeneratesForNonAsyncSelector#SelectorAttribute.g.verified.cs create mode 100644 src/Fluss.UnitTest/Regen/Snapshots/SelectorGeneratorTests.GeneratesForNonAsyncSelector#Selectors.g.verified.cs create mode 100644 src/Fluss.UnitTest/Regen/Snapshots/SelectorGeneratorTests.GeneratesForUnitOfWorkSelector#SelectorAttribute.g.verified.cs create mode 100644 src/Fluss.UnitTest/Regen/Snapshots/SelectorGeneratorTests.GeneratesForUnitOfWorkSelector#Selectors.g.verified.cs create mode 100644 src/Fluss.UnitTest/Setup.cs create mode 100644 src/Fluss/UnitOfWork/IWriteUnitOfWork.cs create mode 100644 src/Fluss/UnitOfWork/UnitOfWorkRecordingProxy.cs diff --git a/src/.config/dotnet-tools.json b/src/.config/dotnet-tools.json new file mode 100644 index 0000000..e056650 --- /dev/null +++ b/src/.config/dotnet-tools.json @@ -0,0 +1,13 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "verify.tool": { + "version": "0.6.0", + "commands": [ + "dotnet-verify" + ], + "rollForward": false + } + } +} \ No newline at end of file diff --git a/src/.gitignore b/src/.gitignore index 43d1bc7..606d593 100644 --- a/src/.gitignore +++ b/src/.gitignore @@ -13,4 +13,6 @@ msbuild.log msbuild.err msbuild.wrn -.idea \ No newline at end of file +.idea + +*.received.* \ No newline at end of file diff --git a/src/Fluss.Regen/Fluss.Regen/Attributes/SelectorAttribute.cs b/src/Fluss.Regen/Attributes/SelectorAttribute.cs similarity index 100% rename from src/Fluss.Regen/Fluss.Regen/Attributes/SelectorAttribute.cs rename to src/Fluss.Regen/Attributes/SelectorAttribute.cs diff --git a/src/Fluss.Regen/Fluss.Regen.Tests/Fluss.Regen.Tests.csproj b/src/Fluss.Regen/Fluss.Regen.Tests/Fluss.Regen.Tests.csproj deleted file mode 100644 index ca7db7f..0000000 --- a/src/Fluss.Regen/Fluss.Regen.Tests/Fluss.Regen.Tests.csproj +++ /dev/null @@ -1,30 +0,0 @@ - - - - net8.0 - enable - - false - - Fluss.Regen.Tests - - - - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - - - - - - - - diff --git a/src/Fluss.Regen/Fluss.Regen.Tests/SampleIncrementalSourceGeneratorTests.cs b/src/Fluss.Regen/Fluss.Regen.Tests/SampleIncrementalSourceGeneratorTests.cs deleted file mode 100644 index c11533f..0000000 --- a/src/Fluss.Regen/Fluss.Regen.Tests/SampleIncrementalSourceGeneratorTests.cs +++ /dev/null @@ -1,77 +0,0 @@ -using Fluss.Regen.Tests.Utils.Extensions; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Xunit; - -namespace Fluss.Regen.Tests; - -public class SampleIncrementalSourceGeneratorTests -{ - [Fact] - public void GeneratesForNonAsyncSelector() - { - var generator = new AutoLoadGenerator(); - - var driver = CSharpGeneratorDriver.Create(generator); - - var compilation = CSharpCompilation.Create(nameof(SampleIncrementalSourceGeneratorTests), - [CSharpSyntaxTree.ParseText( - @" -using Fluss.Regen; - -namespace TestNamespace; - -public class Test -{ - [Selector] - public static int Add(int a, int b) { - return a + b; - } -}")], - new[] - { - MetadataReference.CreateFromFile(typeof(object).Assembly.Location) - }); - - var runResult = driver.RunGenerators(compilation).GetRunResult(); - - runResult.MatchMarkdownSnapshot(); - } - - [Fact] - public void GeneratesForAsyncSelector() - { - var generator = new AutoLoadGenerator(); - - var driver = CSharpGeneratorDriver.Create(generator); - - var compilation = CSharpCompilation.Create(nameof(SampleIncrementalSourceGeneratorTests), - [CSharpSyntaxTree.ParseText( - @" -using Fluss.Regen; -using System.Threading.Tasks; - -namespace TestNamespace; - -public class Test -{ - [Selector] - public static async ValueTask Add(int a, int b) { - return a + b; - } - - [Selector] - public static async ValueTask Add2(int a, int b) { - return a + b; - } -}")], - new[] - { - MetadataReference.CreateFromFile(typeof(object).Assembly.Location) - }); - - var runResult = driver.RunGenerators(compilation).GetRunResult(); - - runResult.MatchMarkdownSnapshot(); - } -} \ No newline at end of file diff --git a/src/Fluss.Regen/Fluss.Regen.Tests/Utils/Extensions/MethodBaseExtension.cs b/src/Fluss.Regen/Fluss.Regen.Tests/Utils/Extensions/MethodBaseExtension.cs deleted file mode 100644 index eb48573..0000000 --- a/src/Fluss.Regen/Fluss.Regen.Tests/Utils/Extensions/MethodBaseExtension.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System.Globalization; -using System.Reflection; - -namespace Fluss.Regen.Tests.Utils.Extensions; - -/// -/// The method base extension is used to add more functionality -/// to the class -/// -internal static class MethodBaseExtension -{ - /// - /// Creates the name of the method with class name. - /// - /// The used method name to get the name. - public static string ToName(this MethodBase methodBase) - => string.Concat( - methodBase.ReflectedType!.Name.ToString(CultureInfo.InvariantCulture), ".", - methodBase.Name.ToString(CultureInfo.InvariantCulture), ".snap"); -} diff --git a/src/Fluss.Regen/Fluss.Regen.Tests/Utils/Extensions/SnapshotExtensions.cs b/src/Fluss.Regen/Fluss.Regen.Tests/Utils/Extensions/SnapshotExtensions.cs deleted file mode 100644 index cb72ade..0000000 --- a/src/Fluss.Regen/Fluss.Regen.Tests/Utils/Extensions/SnapshotExtensions.cs +++ /dev/null @@ -1,32 +0,0 @@ -using Fluss.Regen.Tests.Utils.Formatters; - -namespace Fluss.Regen.Tests.Utils.Extensions; - -public static class SnapshotExtensions -{ - public static void MatchInlineSnapshot( - this object? value, - string snapshot, - ISnapshotValueFormatter? formatter = null) - => Snapshot.Create().Add(value, formatter: formatter).MatchInline(snapshot); - - public static void MatchSnapshot(this Snapshot value) - => value.Match(); - - public static void MatchSnapshot( - this object? value, - object? postFix = null, - string? extension = null, - ISnapshotValueFormatter? formatter = null) - => Snapshot.Match(value, postFix?.ToString(), extension, formatter); - - public static void MatchMarkdownSnapshot( - this object? value, - object? postFix = null, - string? extension = null, - ISnapshotValueFormatter? formatter = null) - => Snapshot.Create(postFix?.ToString(), extension).Add(value, formatter: formatter).MatchMarkdown(); - - public static void MatchMarkdownSnapshot(this Snapshot value) - => value.MatchMarkdown(); -} diff --git a/src/Fluss.Regen/Fluss.Regen.Tests/Utils/Extensions/TypeExtensions.cs b/src/Fluss.Regen/Fluss.Regen.Tests/Utils/Extensions/TypeExtensions.cs deleted file mode 100644 index 0c04d67..0000000 --- a/src/Fluss.Regen/Fluss.Regen.Tests/Utils/Extensions/TypeExtensions.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace Fluss.Regen.Tests.Utils.Extensions; - -/// -/// Some extensions for Type, to support snapshot testing. -/// -internal static class TypeExtensions -{ - /// - /// Returns the list of inherited types. - /// - /// The current object type. - /// The list of all inherited types. - public static IEnumerable BaseTypesAndSelf(this Type type) - { - var current = type; - - while (current != null) - { - yield return current; - current = current.BaseType; - } - } -} diff --git a/src/Fluss.Regen/Fluss.Regen.Tests/Utils/Extensions/WriterExtensions.cs b/src/Fluss.Regen/Fluss.Regen.Tests/Utils/Extensions/WriterExtensions.cs deleted file mode 100644 index 5a8a58b..0000000 --- a/src/Fluss.Regen/Fluss.Regen.Tests/Utils/Extensions/WriterExtensions.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System; -using System.Buffers; -using System.Text; - -namespace Fluss.Regen.Tests.Utils.Extensions; - -public static class WriterExtensions -{ - private static readonly Encoding _utf8 = Encoding.UTF8; - - public static void Append(this IBufferWriter snapshot, string value) - => Append(snapshot, value.AsSpan()); - - public static void Append(this IBufferWriter snapshot, ReadOnlySpan value) - { - _utf8.GetBytes(value, snapshot); - } - - public static void AppendLine(this IBufferWriter snapshot) - { - snapshot.GetSpan(1)[0] = (byte)'\n'; - snapshot.Advance(1); - } - - public static void AppendLine(this IBufferWriter snapshot, bool appendWhenTrue) - { - if (!appendWhenTrue) - { - return; - } - - snapshot.GetSpan(1)[0] = (byte)'\n'; - snapshot.Advance(1); - } - - public static void AppendSeparator(this IBufferWriter snapshot) - { - const byte hyphen = (byte)'-'; - var span = snapshot.GetSpan(15); - - for(var i = 0; i < 15; i++) - { - span[i] = hyphen; - } - - snapshot.Advance(15); - } -} diff --git a/src/Fluss.Regen/Fluss.Regen.Tests/Utils/Formatters/ExceptionSnapshotValueFormatter.cs b/src/Fluss.Regen/Fluss.Regen.Tests/Utils/Formatters/ExceptionSnapshotValueFormatter.cs deleted file mode 100644 index 9b1c07b..0000000 --- a/src/Fluss.Regen/Fluss.Regen.Tests/Utils/Formatters/ExceptionSnapshotValueFormatter.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; -using System.Buffers; -using Fluss.Regen.Tests.Utils.Extensions; - -namespace Fluss.Regen.Tests.Utils.Formatters; - -public class ExceptionSnapshotValueFormatter : SnapshotValueFormatter -{ - protected override void Format(IBufferWriter snapshot, Exception value) - { - snapshot.Append(value.GetType().FullName ?? value.GetType().Name); - snapshot.AppendLine(); - snapshot.Append(value.Message); - snapshot.AppendLine(); - } -} diff --git a/src/Fluss.Regen/Fluss.Regen.Tests/Utils/Formatters/GeneratorDriverRunResultSnapshotValueFormatter.cs b/src/Fluss.Regen/Fluss.Regen.Tests/Utils/Formatters/GeneratorDriverRunResultSnapshotValueFormatter.cs deleted file mode 100644 index d41a12d..0000000 --- a/src/Fluss.Regen/Fluss.Regen.Tests/Utils/Formatters/GeneratorDriverRunResultSnapshotValueFormatter.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System; -using System.Buffers; -using Fluss.Regen.Tests.Utils.Extensions; -using Microsoft.CodeAnalysis; - -namespace Fluss.Regen.Tests.Utils.Formatters; - -public class GeneratorDriverRunResultSnapshotValueFormatter : SnapshotValueFormatter -{ - protected override void Format(IBufferWriter snapshot, GeneratorDriverRunResult value) - { - throw new NotImplementedException(); - } - - protected override void FormatMarkdown(IBufferWriter snapshot, GeneratorDriverRunResult value) - { - foreach (var tree in value.GeneratedTrees) - { - snapshot.Append($"## {tree.FilePath}"); - snapshot.AppendLine(); - snapshot.Append("```csharp"); - snapshot.AppendLine(); - snapshot.Append(tree.GetText().ToString()); - snapshot.AppendLine(); - snapshot.Append("```"); - snapshot.AppendLine(); - snapshot.AppendLine(); - } - } -} diff --git a/src/Fluss.Regen/Fluss.Regen.Tests/Utils/Formatters/HttpResponseSnapshotValueFormatter.cs b/src/Fluss.Regen/Fluss.Regen.Tests/Utils/Formatters/HttpResponseSnapshotValueFormatter.cs deleted file mode 100644 index 873e3da..0000000 --- a/src/Fluss.Regen/Fluss.Regen.Tests/Utils/Formatters/HttpResponseSnapshotValueFormatter.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System.Buffers; -using System.Linq; -using System.Net.Http; -using Fluss.Regen.Tests.Utils.Extensions; - -namespace Fluss.Regen.Tests.Utils.Formatters; - -public class HttpResponseSnapshotValueFormatter : SnapshotValueFormatter -{ - protected override void Format(IBufferWriter snapshot, HttpResponseMessage value) - { - var first = true; - - foreach (var header in value.Headers.Concat(value.Content.Headers)) - { - if (first) - { - snapshot.Append("Headers:"); - snapshot.AppendLine(); - first = false; - } - - snapshot.Append($"{header.Key}: {string.Join(" ", header.Value)}"); - snapshot.AppendLine(); - } - - if (!first) - { - snapshot.Append("-------------------------->"); - snapshot.AppendLine(); - } - - snapshot.Append($"Status Code: {value.StatusCode}"); - - snapshot.AppendLine(); - snapshot.Append("-------------------------->"); - snapshot.AppendLine(); - - snapshot.Append(value.Content.ReadAsStringAsync().Result); - } -} diff --git a/src/Fluss.Regen/Fluss.Regen.Tests/Utils/Formatters/IMarkdownSnapshotValueFormatter.cs b/src/Fluss.Regen/Fluss.Regen.Tests/Utils/Formatters/IMarkdownSnapshotValueFormatter.cs deleted file mode 100644 index d710a09..0000000 --- a/src/Fluss.Regen/Fluss.Regen.Tests/Utils/Formatters/IMarkdownSnapshotValueFormatter.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System.Buffers; - -namespace Fluss.Regen.Tests.Utils.Formatters; - -/// -/// Formats a snapshot segment value for the snapshot file. -/// -public interface IMarkdownSnapshotValueFormatter -{ - /// - /// Specifies if the formatter can handle the snapshot segment value. - /// - /// - /// The snapshot segment value. - /// - /// - /// true if the formatter can handle the snapshot segment value; - /// otherwise, false. - /// - bool CanHandle(object? value); - - /// - /// Formats the specified snapshot segment value for the snapshot file. - /// - /// - /// The snapshot file writer. - /// - /// - /// The snapshot segment vale. - /// - void FormatMarkdown(IBufferWriter snapshot, object? value); -} \ No newline at end of file diff --git a/src/Fluss.Regen/Fluss.Regen.Tests/Utils/Formatters/ISnapshotValueFormatter.cs b/src/Fluss.Regen/Fluss.Regen.Tests/Utils/Formatters/ISnapshotValueFormatter.cs deleted file mode 100644 index f9c6dc8..0000000 --- a/src/Fluss.Regen/Fluss.Regen.Tests/Utils/Formatters/ISnapshotValueFormatter.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System.Buffers; - -namespace Fluss.Regen.Tests.Utils.Formatters; - -/// -/// Formats a snapshot segment value for the snapshot file. -/// -public interface ISnapshotValueFormatter -{ - /// - /// Specifies if the formatter can handle the snapshot segment value. - /// - /// - /// The snapshot segment value. - /// - /// - /// true if the formatter can handle the snapshot segment value; - /// otherwise, false. - /// - bool CanHandle(object? value); - - /// - /// Formats the specified snapshot segment value for the snapshot file. - /// - /// - /// The snapshot file writer. - /// - /// - /// The snapshot segment vale. - /// - void Format(IBufferWriter snapshot, object? value); -} \ No newline at end of file diff --git a/src/Fluss.Regen/Fluss.Regen.Tests/Utils/Formatters/JsonSnapshotValueFormatter.cs b/src/Fluss.Regen/Fluss.Regen.Tests/Utils/Formatters/JsonSnapshotValueFormatter.cs deleted file mode 100644 index 91042eb..0000000 --- a/src/Fluss.Regen/Fluss.Regen.Tests/Utils/Formatters/JsonSnapshotValueFormatter.cs +++ /dev/null @@ -1,64 +0,0 @@ -using System; -using System.Buffers; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using Fluss.Regen.Tests.Utils.Extensions; -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; -using Newtonsoft.Json.Serialization; - -namespace Fluss.Regen.Tests.Utils.Formatters; - -internal sealed class JsonSnapshotValueFormatter : ISnapshotValueFormatter, IMarkdownSnapshotValueFormatter -{ - private static readonly JsonSerializerSettings _settings = - new() - { - ReferenceLoopHandling = ReferenceLoopHandling.Ignore, - Formatting = Formatting.Indented, - NullValueHandling = NullValueHandling.Include, - DateFormatHandling = DateFormatHandling.IsoDateFormat, - Culture = CultureInfo.InvariantCulture, - ContractResolver = ChildFirstContractResolver.Instance, - Converters = new List { new StringEnumConverter(), }, - }; - - public bool CanHandle(object? value) - => true; - - public void Format(IBufferWriter snapshot, object? value) - => snapshot.Append(JsonConvert.SerializeObject(value, _settings)); - - public void FormatMarkdown(IBufferWriter snapshot, object? value) - { - snapshot.Append("```json"); - snapshot.AppendLine(); - Format(snapshot, value); - snapshot.AppendLine(); - snapshot.Append("```"); - snapshot.AppendLine(); - } - - private class ChildFirstContractResolver : DefaultContractResolver - { - static ChildFirstContractResolver() { Instance = new ChildFirstContractResolver(); } - - public static ChildFirstContractResolver Instance { get; } - - protected override IList CreateProperties( - Type type, - MemberSerialization memberSerialization) - { - var properties = base.CreateProperties(type, memberSerialization); - - properties = properties.OrderBy(p => - { - var d = p.DeclaringType!.BaseTypesAndSelf().ToList(); - return 1000 - d.Count; - }).ToList(); - - return properties!; - } - } -} diff --git a/src/Fluss.Regen/Fluss.Regen.Tests/Utils/Formatters/PlainTextSnapshotValueFormatter.cs b/src/Fluss.Regen/Fluss.Regen.Tests/Utils/Formatters/PlainTextSnapshotValueFormatter.cs deleted file mode 100644 index ef24426..0000000 --- a/src/Fluss.Regen/Fluss.Regen.Tests/Utils/Formatters/PlainTextSnapshotValueFormatter.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.Buffers; -using Fluss.Regen.Tests.Utils.Extensions; - -namespace Fluss.Regen.Tests.Utils.Formatters; - -internal sealed class PlainTextSnapshotValueFormatter : ISnapshotValueFormatter -{ - public bool CanHandle(object? value) - => value is string; - - public void Format(IBufferWriter snapshot, object? value) - { - if (value?.ToString() is { } s) - { - snapshot.Append(s); - } - } -} diff --git a/src/Fluss.Regen/Fluss.Regen.Tests/Utils/Formatters/SnapshotValueFormatter.cs b/src/Fluss.Regen/Fluss.Regen.Tests/Utils/Formatters/SnapshotValueFormatter.cs deleted file mode 100644 index 97096b8..0000000 --- a/src/Fluss.Regen/Fluss.Regen.Tests/Utils/Formatters/SnapshotValueFormatter.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Buffers; -using Fluss.Regen.Tests.Utils.Extensions; - -namespace Fluss.Regen.Tests.Utils.Formatters; - -/// -/// Formats a snapshot segment value for the snapshot file. -/// -public abstract class SnapshotValueFormatter(string markdownKind = "text") : ISnapshotValueFormatter - , IMarkdownSnapshotValueFormatter -{ - public bool CanHandle(object? value) - => value is TValue casted && CanHandle(casted); - - protected virtual bool CanHandle(TValue? value) - => true; - - public void Format(IBufferWriter snapshot, object? value) - => Format(snapshot, (TValue)value!); - - public void FormatMarkdown(IBufferWriter snapshot, object? value) - => FormatMarkdown(snapshot, (TValue)value!); - - protected abstract void Format(IBufferWriter snapshot, TValue value); - - protected virtual void FormatMarkdown(IBufferWriter snapshot, TValue value) - { - snapshot.Append($"```{markdownKind}"); - snapshot.AppendLine(); - Format(snapshot, value); - snapshot.AppendLine(); - snapshot.Append("```"); - snapshot.AppendLine(); - } -} diff --git a/src/Fluss.Regen/Fluss.Regen.Tests/Utils/ISnapshotSegment.cs b/src/Fluss.Regen/Fluss.Regen.Tests/Utils/ISnapshotSegment.cs deleted file mode 100644 index b78dcff..0000000 --- a/src/Fluss.Regen/Fluss.Regen.Tests/Utils/ISnapshotSegment.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Fluss.Regen.Tests.Utils; - -internal interface ISnapshotSegment -{ - string? Name { get; } -} diff --git a/src/Fluss.Regen/Fluss.Regen.Tests/Utils/Snapshot.cs b/src/Fluss.Regen/Fluss.Regen.Tests/Utils/Snapshot.cs deleted file mode 100644 index 5e243e6..0000000 --- a/src/Fluss.Regen/Fluss.Regen.Tests/Utils/Snapshot.cs +++ /dev/null @@ -1,545 +0,0 @@ -using System; -using System.Buffers; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Text; -using DiffPlex.DiffBuilder; -using DiffPlex.DiffBuilder.Model; -using Fluss.Regen.Tests.Utils.Extensions; -using Fluss.Regen.Tests.Utils.Formatters; -using Xunit; -using static System.IO.Path; - -namespace Fluss.Regen.Tests.Utils; - -public class Snapshot -{ - private static readonly UTF8Encoding Encoding = new(); - private static readonly ImmutableStack Formatters = - ImmutableStack.CreateRange(new ISnapshotValueFormatter[] - { - new PlainTextSnapshotValueFormatter(), - new ExceptionSnapshotValueFormatter(), - new HttpResponseSnapshotValueFormatter(), - new GeneratorDriverRunResultSnapshotValueFormatter(), - }); - private static readonly JsonSnapshotValueFormatter DefaultFormatter = new(); - - private readonly List _segments = []; - private readonly string _title; - private readonly string _fileName; - private readonly string _extension; - private readonly string? _postFix; - - public Snapshot(string? postFix = null, string? extension = null) - { - var frames = new StackTrace(true).GetFrames(); - _title = CreateMarkdownTitle(frames); - _fileName = CreateFileName(frames); - _postFix = postFix; - _extension = extension ?? ".snap"; - } - - public static Snapshot Create(string? postFix = null, string? extension = null) - => new(postFix, extension); - - public static DisposableSnapshot Start(string? postFix = null, string? extension = null) - => new(postFix, extension); - - public static void Match( - object? value, - string? postFix = null, - string? extension = null, - ISnapshotValueFormatter? formatter = null) - { - var snapshot = new Snapshot(postFix, extension); - snapshot.Add(value, formatter: formatter); - snapshot.Match(); - } - - public Snapshot Add( - object? value, - string? name = null, - ISnapshotValueFormatter? formatter = null) - { - formatter ??= FindSerializer(value); - _segments.Add(new SnapshotSegment(name, value, formatter)); - return this; - } - - private static ISnapshotValueFormatter FindSerializer(object? value) - { - // we capture the current immutable serializer list - var serializers = Formatters; - - // the we iterate over the captured stack. - foreach (var serializer in serializers) - { - if (serializer.CanHandle(value)) - { - return serializer; - } - } - - return DefaultFormatter; - } - - public void Match() - { - var writer = new ArrayBufferWriter(); - WriteSegments(writer); - - var snapshotFile = Combine(CreateSnapshotDirectoryName(), CreateSnapshotFileName()); - - if (!File.Exists(snapshotFile)) - { - EnsureDirectoryExists(snapshotFile); - using var stream = File.Create(snapshotFile); - stream.Write(writer.WrittenSpan); - } - else - { - var mismatchFile = Combine(CreateMismatchDirectoryName(), CreateSnapshotFileName()); - EnsureFileDoesNotExist(mismatchFile); - var before = File.ReadAllText(snapshotFile); - var after = Encoding.GetString(writer.WrittenSpan); - - if (!MatchSnapshot(before, after, false, out var diff)) - { - EnsureDirectoryExists(mismatchFile); - using var stream = File.Create(mismatchFile); - stream.Write(writer.WrittenSpan); - throw new Xunit.Sdk.XunitException(diff); - } - } - } - - public void MatchMarkdown() - { - var writer = new ArrayBufferWriter(); - - writer.Append($"# {_title}"); - writer.AppendLine(); - writer.AppendLine(); - - WriteMarkdownSegments(writer); - - var snapshotFile = Combine(CreateSnapshotDirectoryName(), CreateMarkdownSnapshotFileName()); - - if (!File.Exists(snapshotFile)) - { - EnsureDirectoryExists(snapshotFile); - using var stream = File.Create(snapshotFile); - stream.Write(writer.WrittenSpan); - } - else - { - var mismatchFile = Combine(CreateMismatchDirectoryName(), CreateMarkdownSnapshotFileName()); - EnsureFileDoesNotExist(mismatchFile); - var before = File.ReadAllText(snapshotFile); - var after = Encoding.GetString(writer.WrittenSpan); - - if (MatchSnapshot(before, after, false, out var diff)) - { - return; - } - - EnsureDirectoryExists(mismatchFile); - using var stream = File.Create(mismatchFile); - stream.Write(writer.WrittenSpan); - throw new Xunit.Sdk.XunitException(diff); - } - } - - public void MatchInline(string expected) - { - var writer = new ArrayBufferWriter(); - WriteSegments(writer); - - var after = Encoding.GetString(writer.WrittenSpan); - - if (!MatchSnapshot(expected, after, true, out var diff)) - { - throw new Xunit.Sdk.XunitException(diff); - } - } - - private void WriteSegments(IBufferWriter writer) - { - if (_segments.Count == 1) - { - switch (_segments[0]) - { - case SnapshotSegment segment: - segment.Formatter.Format(writer, segment.Value); - break; - case SnapshotValue value: - writer.Write(value.Value); - break; - default: - throw new NotSupportedException(); - } - return; - } - - var next = false; - - foreach (var segment in _segments) - { - if (next) - { - writer.AppendLine(); - } - - if (!string.IsNullOrEmpty(segment.Name)) - { - writer.Append(segment.Name); - writer.AppendLine(); - } - - writer.AppendSeparator(); - writer.AppendLine(); - - switch (segment) - { - case SnapshotSegment s: - s.Formatter.Format(writer, s.Value); - break; - case SnapshotValue v: - writer.Write(v.Value); - break; - default: - throw new NotSupportedException(); - } - - writer.AppendLine(); - writer.AppendSeparator(); - writer.AppendLine(); - - next = true; - } - } - - private void WriteMarkdownSegments(IBufferWriter writer) - { - if (_segments.Count == 1) - { - var segment = _segments[0]; - - switch (segment) - { - case SnapshotSegment s: - if (s.Formatter is IMarkdownSnapshotValueFormatter markdownFormatter) - { - markdownFormatter.FormatMarkdown(writer, s.Value); - } - else - { - writer.Append("```text"); - writer.AppendLine(); - s.Formatter.Format(writer, s.Value); - writer.AppendLine(); - writer.Append("```"); - writer.AppendLine(); - } - break; - case SnapshotValue v: - v.FormatMarkdown(writer); - break; - default: - throw new NotSupportedException(); - } - return; - } - - var i = 0; - foreach (var segment in _segments) - { - i++; - writer.Append( - string.IsNullOrEmpty(segment.Name) - ? $"## Result {i}" - : $"## {segment.Name}"); - writer.AppendLine(); - writer.AppendLine(); - - switch (segment) - { - case SnapshotSegment s: - if (s.Formatter is IMarkdownSnapshotValueFormatter markdownFormatter) - { - markdownFormatter.FormatMarkdown(writer, s.Value); - } - else - { - writer.Append("```text"); - writer.AppendLine(); - s.Formatter.Format(writer, s.Value); - writer.AppendLine(); - writer.Append("```"); - writer.AppendLine(); - } - break; - case SnapshotValue v: - v.FormatMarkdown(writer); - break; - default: - throw new NotSupportedException(); - } - - writer.AppendLine(); - } - } - - private static bool MatchSnapshot( - string before, - string after, - bool inline, - [NotNullWhen(false)] out string? snapshotDiff) - { - var diff = InlineDiffBuilder.Diff(before, after); - - if (diff.HasDifferences) - { - var output = new StringBuilder(); - output.AppendLine("The snapshot does not match:"); - - foreach (var line in diff.Lines) - { - switch (line.Type) - { - case ChangeType.Inserted: - output.Append("+ "); - break; - - case ChangeType.Deleted: - output.Append("- "); - break; - - default: - output.Append(" "); - break; - } - - output.AppendLine(line.Text); - } - - if (inline) - { - output.AppendLine(); - output.AppendLine("The new snapshot:"); - output.AppendLine(after); - } - - snapshotDiff = output.ToString(); - return false; - } - - snapshotDiff = null; - return true; - } - - private string CreateMismatchDirectoryName() - => CreateSnapshotDirectoryName(true); - - private string CreateSnapshotDirectoryName(bool mismatch = false) - { - var directoryName = GetDirectoryName(_fileName)!; - - return mismatch - ? Combine(directoryName, "__snapshots__", "__MISMATCH__") - : Combine(directoryName, "__snapshots__"); - } - - private static void EnsureDirectoryExists(string file) - { - try - { - var directory = GetDirectoryName(file)!; - - if (!Directory.Exists(directory)) - { - Directory.CreateDirectory(directory); - } - } - catch - { - // we ignore exception that could happen due to collisions - } - } - - private static void EnsureFileDoesNotExist(string file) - { - if (File.Exists(file)) - { - File.Delete(file); - } - } - - private string CreateSnapshotFileName() - { - var fileName = GetFileNameWithoutExtension(_fileName); - - return string.IsNullOrEmpty(_postFix) - ? string.Concat(fileName, _extension) - : string.Concat(fileName, "_", _postFix, _extension); - } - - private string CreateMarkdownSnapshotFileName() - { - var extension = _extension.Equals(".snap", StringComparison.Ordinal) ? ".md" : _extension; - - var fileName = GetFileNameWithoutExtension(_fileName); - - return string.IsNullOrEmpty(_postFix) - ? string.Concat(fileName, extension) - : string.Concat(fileName, "_", _postFix, extension); - } - - private static string CreateFileName(StackFrame[] frames) - { - foreach (var stackFrame in frames) - { - var method = stackFrame.GetMethod(); - var fileName = stackFrame.GetFileName(); - - if (method is not null && - !string.IsNullOrEmpty(fileName) && - IsXunitTestMethod(method)) - { - return Combine(GetDirectoryName(fileName)!, method.ToName()); - } - - var asyncMethod = EvaluateAsynchronousMethodBase(method); - - if (asyncMethod is not null && - !string.IsNullOrEmpty(fileName) && - IsXunitTestMethod(asyncMethod)) - { - return Combine(GetDirectoryName(fileName)!, asyncMethod.ToName()); - } - } - - throw new Exception( - "The snapshot full name could not be evaluated. " + - "This error can occur, if you use the snapshot match " + - "within a async test helper child method. To solve this issue, " + - "use the Snapshot.FullName directly in the unit test to " + - "get the snapshot name, then reach this name to your " + - "Snapshot.Match method."); - } - - private static string CreateMarkdownTitle(StackFrame[] frames) - { - foreach (var stackFrame in frames) - { - var method = stackFrame.GetMethod(); - var fileName = stackFrame.GetFileName(); - - if (method is not null && - !string.IsNullOrEmpty(fileName) && - IsXunitTestMethod(method)) - { - return method.Name; - } - - var asyncMethod = EvaluateAsynchronousMethodBase(method); - - if (asyncMethod is not null && - !string.IsNullOrEmpty(fileName) && - IsXunitTestMethod(asyncMethod)) - { - return asyncMethod.Name; - } - } - - throw new Exception( - "The snapshot full name could not be evaluated. " + - "This error can occur, if you use the snapshot match " + - "within a async test helper child method. To solve this issue, " + - "use the Snapshot.FullName directly in the unit test to " + - "get the snapshot name, then reach this name to your " + - "Snapshot.Match method."); - } - - private static MethodBase? EvaluateAsynchronousMethodBase(MemberInfo? method) - { - var methodDeclaringType = method?.DeclaringType; - var classDeclaringType = methodDeclaringType?.DeclaringType; - - MethodInfo? actualMethodInfo = null; - - if (classDeclaringType != null) - { - var selectedMethodInfos = - from methodInfo in classDeclaringType.GetMethods() - let stateMachineAttribute = methodInfo - .GetCustomAttribute() - where stateMachineAttribute != null && - stateMachineAttribute.StateMachineType == methodDeclaringType - select methodInfo; - - actualMethodInfo = selectedMethodInfos.SingleOrDefault(); - } - - return actualMethodInfo; - } - - private static bool IsXunitTestMethod(MemberInfo? method) - { - var isFactTest = IsFactTestMethod(method); - var isTheoryTest = IsTheoryTestMethod(method); - - return isFactTest || isTheoryTest; - } - - private static bool IsFactTestMethod(MemberInfo? method) - => method?.GetCustomAttributes(typeof(FactAttribute)).Any() ?? false; - - private static bool IsTheoryTestMethod(MemberInfo? method) - => method?.GetCustomAttributes(typeof(TheoryAttribute)).Any() ?? false; - - private readonly struct SnapshotSegment(string? name, object? value, ISnapshotValueFormatter formatter) - : ISnapshotSegment - { - public string? Name { get; } = name; - - public object? Value { get; } = value; - - public ISnapshotValueFormatter Formatter { get; } = formatter; - } -} - - -public sealed class DisposableSnapshot(string? postFix = null, string? extension = null) - : Snapshot(postFix, extension) - , IDisposable -{ - public void Dispose() => Match(); -} - -public abstract class SnapshotValue : ISnapshotSegment -{ - public abstract string? Name { get; } - - public abstract ReadOnlySpan Value { get; } - - protected virtual string MarkdownType => "text"; - - public virtual void FormatMarkdown(IBufferWriter snapshot) - { - snapshot.Append("```"); - snapshot.Append(MarkdownType); - snapshot.AppendLine(); - snapshot.Write(Value); - snapshot.AppendLine(); - snapshot.Append("```"); - snapshot.AppendLine(); - } -} diff --git a/src/Fluss.Regen/Fluss.Regen.Tests/Utils/TestAdditionalFile.cs b/src/Fluss.Regen/Fluss.Regen.Tests/Utils/TestAdditionalFile.cs deleted file mode 100644 index 1037b42..0000000 --- a/src/Fluss.Regen/Fluss.Regen.Tests/Utils/TestAdditionalFile.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System.Threading; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Text; - -namespace Fluss.Regen.Tests.Utils; - -public class TestAdditionalFile(string path, string text) : AdditionalText -{ - private readonly SourceText _text = SourceText.From(text); - - public override SourceText GetText(CancellationToken cancellationToken = new()) => _text; - - public override string Path { get; } = path; -} \ No newline at end of file diff --git a/src/Fluss.Regen/Fluss.Regen.Tests/__snapshots__/SampleIncrementalSourceGeneratorTests.GeneratesForAsyncSelector.md b/src/Fluss.Regen/Fluss.Regen.Tests/__snapshots__/SampleIncrementalSourceGeneratorTests.GeneratesForAsyncSelector.md deleted file mode 100644 index 402ce24..0000000 --- a/src/Fluss.Regen/Fluss.Regen.Tests/__snapshots__/SampleIncrementalSourceGeneratorTests.GeneratesForAsyncSelector.md +++ /dev/null @@ -1,70 +0,0 @@ -# GeneratesForAsyncSelector - -## Fluss.Regen\Fluss.Regen.AutoLoadGenerator\SelectorAttribute.g.cs -```csharp -// - -namespace Fluss.Regen -{ - [System.AttributeUsage(System.AttributeTargets.Method)] - public class SelectorAttribute : System.Attribute - { - } -} -``` - -## Fluss.Regen\Fluss.Regen.AutoLoadGenerator\SelectorInterfaces.g.cs -```csharp -// - -#nullable enable - -using System; -using System.Runtime.CompilerServices; -using Microsoft.Extensions.DependencyInjection; - -namespace Fluss -{ - public partial interface IUnitOfWork - { - public global::System.Threading.Tasks.ValueTask SelectAdd( - int a, - int b - ); - } -} -} - - -``` - -## Fluss.Regen\Fluss.Regen.AutoLoadGenerator\Selectors.g.cs -```csharp -// - -#nullable enable - -using System; -using System.Runtime.CompilerServices; -using Microsoft.Extensions.DependencyInjection; - -namespace Fluss -{ - public partial class UnitOfWork - { - public async global::System.Threading.Tasks.ValueTask SelectAdd( - int a, - int b - ) - { - return await global::TestNamespace.Test.Add( - a, - b - ).ConfigureAwait(false); - } - } -} - - -``` - diff --git a/src/Fluss.Regen/Fluss.Regen.Tests/__snapshots__/SampleIncrementalSourceGeneratorTests.GeneratesForNonAsyncSelector.md b/src/Fluss.Regen/Fluss.Regen.Tests/__snapshots__/SampleIncrementalSourceGeneratorTests.GeneratesForNonAsyncSelector.md deleted file mode 100644 index 1e76627..0000000 --- a/src/Fluss.Regen/Fluss.Regen.Tests/__snapshots__/SampleIncrementalSourceGeneratorTests.GeneratesForNonAsyncSelector.md +++ /dev/null @@ -1,70 +0,0 @@ -# GeneratesForNonAsyncSelector - -## Fluss.Regen\Fluss.Regen.AutoLoadGenerator\SelectorAttribute.g.cs -```csharp -// - -namespace Fluss.Regen -{ - [System.AttributeUsage(System.AttributeTargets.Method)] - public class SelectorAttribute : System.Attribute - { - } -} -``` - -## Fluss.Regen\Fluss.Regen.AutoLoadGenerator\SelectorInterfaces.g.cs -```csharp -// - -#nullable enable - -using System; -using System.Runtime.CompilerServices; -using Microsoft.Extensions.DependencyInjection; - -namespace Fluss -{ - public partial interface IUnitOfWork - { - public global::System.Threading.Tasks.ValueTask SelectAdd( - int a, - int b - ); - } -} -} - - -``` - -## Fluss.Regen\Fluss.Regen.AutoLoadGenerator\Selectors.g.cs -```csharp -// - -#nullable enable - -using System; -using System.Runtime.CompilerServices; -using Microsoft.Extensions.DependencyInjection; - -namespace Fluss -{ - public partial class UnitOfWork - { - public async global::System.Threading.Tasks.ValueTask SelectAdd( - int a, - int b - ) - { - return global::TestNamespace.Test.Add( - a, - b - ); - } - } -} - - -``` - diff --git a/src/Fluss.Regen/Fluss.Regen.Tests/__snapshots__/__MISMATCH__/SampleIncrementalSourceGeneratorTests.GeneratesForAsyncSelector.md b/src/Fluss.Regen/Fluss.Regen.Tests/__snapshots__/__MISMATCH__/SampleIncrementalSourceGeneratorTests.GeneratesForAsyncSelector.md deleted file mode 100644 index 1056f19..0000000 --- a/src/Fluss.Regen/Fluss.Regen.Tests/__snapshots__/__MISMATCH__/SampleIncrementalSourceGeneratorTests.GeneratesForAsyncSelector.md +++ /dev/null @@ -1,83 +0,0 @@ -# GeneratesForAsyncSelector - -## Fluss.Regen\Fluss.Regen.AutoLoadGenerator\SelectorAttribute.g.cs -```csharp -// - -namespace Fluss.Regen -{ - [System.AttributeUsage(System.AttributeTargets.Method)] - public class SelectorAttribute : System.Attribute - { - } -} -``` - -## Fluss.Regen\Fluss.Regen.AutoLoadGenerator\SelectorInterfaces.g.cs -```csharp -// - -#nullable enable - -using System; -using System.Runtime.CompilerServices; -using Microsoft.Extensions.DependencyInjection; - -namespace Fluss -{ - public partial interface IUnitOfWork - { - public global::System.Threading.Tasks.ValueTask SelectAdd( - int a, - int b - ); - public global::System.Threading.Tasks.ValueTask SelectAdd2( - int a, - int b - ); - } -} - - -``` - -## Fluss.Regen\Fluss.Regen.AutoLoadGenerator\Selectors.g.cs -```csharp -// - -#nullable enable - -using System; -using System.Runtime.CompilerServices; -using Microsoft.Extensions.DependencyInjection; - -namespace Fluss -{ - public partial class UnitOfWork - { - public async global::System.Threading.Tasks.ValueTask SelectAdd( - int a, - int b - ) - { - return await global::TestNamespace.Test.Add( - a, - b - ).ConfigureAwait(false); - } - public async global::System.Threading.Tasks.ValueTask SelectAdd2( - int a, - int b - ) - { - return await global::TestNamespace.Test.Add2( - a, - b - ).ConfigureAwait(false); - } - } -} - - -``` - diff --git a/src/Fluss.Regen/Fluss.Regen.Tests/__snapshots__/__MISMATCH__/SampleIncrementalSourceGeneratorTests.GeneratesForNonAsyncSelector.md b/src/Fluss.Regen/Fluss.Regen.Tests/__snapshots__/__MISMATCH__/SampleIncrementalSourceGeneratorTests.GeneratesForNonAsyncSelector.md deleted file mode 100644 index 2aabd8a..0000000 --- a/src/Fluss.Regen/Fluss.Regen.Tests/__snapshots__/__MISMATCH__/SampleIncrementalSourceGeneratorTests.GeneratesForNonAsyncSelector.md +++ /dev/null @@ -1,69 +0,0 @@ -# GeneratesForNonAsyncSelector - -## Fluss.Regen\Fluss.Regen.AutoLoadGenerator\SelectorAttribute.g.cs -```csharp -// - -namespace Fluss.Regen -{ - [System.AttributeUsage(System.AttributeTargets.Method)] - public class SelectorAttribute : System.Attribute - { - } -} -``` - -## Fluss.Regen\Fluss.Regen.AutoLoadGenerator\SelectorInterfaces.g.cs -```csharp -// - -#nullable enable - -using System; -using System.Runtime.CompilerServices; -using Microsoft.Extensions.DependencyInjection; - -namespace Fluss -{ - public partial interface IUnitOfWork - { - public global::System.Threading.Tasks.ValueTask SelectAdd( - int a, - int b - ); - } -} - - -``` - -## Fluss.Regen\Fluss.Regen.AutoLoadGenerator\Selectors.g.cs -```csharp -// - -#nullable enable - -using System; -using System.Runtime.CompilerServices; -using Microsoft.Extensions.DependencyInjection; - -namespace Fluss -{ - public partial class UnitOfWork - { - public async global::System.Threading.Tasks.ValueTask SelectAdd( - int a, - int b - ) - { - return global::TestNamespace.Test.Add( - a, - b - ); - } - } -} - - -``` - diff --git a/src/Fluss.Regen/Fluss.Regen/Fluss.Regen.csproj b/src/Fluss.Regen/Fluss.Regen.csproj similarity index 91% rename from src/Fluss.Regen/Fluss.Regen/Fluss.Regen.csproj rename to src/Fluss.Regen/Fluss.Regen.csproj index 2891055..585dd15 100644 --- a/src/Fluss.Regen/Fluss.Regen/Fluss.Regen.csproj +++ b/src/Fluss.Regen/Fluss.Regen.csproj @@ -20,6 +20,7 @@ + diff --git a/src/Fluss.Regen/Fluss.Regen/Generators/ModuleSyntaxGenerator.cs b/src/Fluss.Regen/Fluss.Regen/Generators/ModuleSyntaxGenerator.cs deleted file mode 100644 index 50e89a1..0000000 --- a/src/Fluss.Regen/Fluss.Regen/Generators/ModuleSyntaxGenerator.cs +++ /dev/null @@ -1,198 +0,0 @@ -using System; -using System.Text; -using Fluss.Regen.Helpers; -using Microsoft.CodeAnalysis.Text; - -namespace Fluss.Regen.Generators; - -public sealed class ModuleSyntaxGenerator : IDisposable -{ - private readonly string _moduleName; - private readonly string _ns; - private StringBuilder _sb; - private CodeWriter _writer; - private bool _disposed; - - public ModuleSyntaxGenerator(string moduleName, string ns) - { - _moduleName = moduleName; - _ns = ns; - _sb = StringBuilderPool.Get(); - _writer = new CodeWriter(_sb); - } - - public void WriterHeader() - { - _writer.WriteFileHeader(); - _writer.WriteLine(); - } - - public void WriteBeginNamespace() - { - _writer.WriteIndentedLine("namespace {0}", _ns); - _writer.WriteIndentedLine("{"); - _writer.IncreaseIndent(); - } - - public void WriteEndNamespace() - { - _writer.DecreaseIndent(); - _writer.WriteIndentedLine("}"); - } - - public void WriteBeginClass() - { - _writer.WriteIndentedLine("public static partial class {0}RequestExecutorBuilderExtensions", _moduleName); - _writer.WriteIndentedLine("{"); - _writer.IncreaseIndent(); - } - - public void WriteEndClass() - { - _writer.DecreaseIndent(); - _writer.WriteIndentedLine("}"); - } - - public void WriteBeginRegistrationMethod() - { - _writer.WriteIndentedLine( - "public static IRequestExecutorBuilder Add{0}(this IRequestExecutorBuilder builder)", - _moduleName); - _writer.WriteIndentedLine("{"); - _writer.IncreaseIndent(); - } - - public void WriteEndRegistrationMethod() - { - _writer.WriteIndentedLine("return builder;"); - _writer.DecreaseIndent(); - _writer.WriteIndentedLine("}"); - } - - public void WriteRegisterType(string typeName) - => _writer.WriteIndentedLine("builder.AddType();", typeName); - - public void WriteRegisterTypeExtension(string typeName, bool staticType) - => _writer.WriteIndentedLine( - staticType - ? "builder.AddTypeExtension(typeof(global::{0}));" - : "builder.AddTypeExtension();", - typeName); - - public void WriteRegisterObjectTypeExtension(string runtimeTypeName, string extensionType) - { - _writer.WriteIndentedLine( - "AddTypeExtension_8734371<{0}>(builder, {1}.Initialize);", - runtimeTypeName, - extensionType); - } - - public void WriteRegisterObjectTypeExtensionHelpers() - { - _writer.WriteLine(); - _writer.WriteIndentedLine("private static void AddTypeExtension_8734371("); - - using (_writer.IncreaseIndent()) - { - _writer.WriteIndentedLine("global::HotChocolate.Execution.Configuration.IRequestExecutorBuilder builder,"); - _writer.WriteIndentedLine("Action> initialize)"); - } - - _writer.WriteIndentedLine("{"); - - using (_writer.IncreaseIndent()) - { - _writer.WriteIndentedLine("builder.ConfigureSchema(sb =>"); - _writer.WriteIndentedLine("{"); - - using (_writer.IncreaseIndent()) - { - _writer.WriteIndentedLine("string typeName = typeof(T).FullName!;"); - _writer.WriteIndentedLine("string typeKey = $\"8734371_Type_ObjectType<{typeName}>\";"); - _writer.WriteIndentedLine("string hooksKey = $\"8734371_Hooks_ObjectType<{typeName}>\";"); - _writer.WriteLine(); - _writer.WriteIndentedLine("if (!sb.ContextData.ContainsKey(typeKey))"); - _writer.WriteIndentedLine("{"); - - using (_writer.IncreaseIndent()) - { - _writer.WriteIndentedLine("sb.AddObjectType("); - using (_writer.IncreaseIndent()) - { - _writer.WriteIndentedLine("descriptor =>"); - _writer.WriteIndentedLine("{"); - - using (_writer.IncreaseIndent()) - { - _writer.WriteIndentedLine( - "var hooks = (global::System.Collections.Generic.List<" + - "Action>>)" + - "descriptor.Extend().Context.ContextData[hooksKey]!;"); - _writer.WriteIndentedLine("foreach (var configure in hooks)"); - _writer.WriteIndentedLine("{"); - - using (_writer.IncreaseIndent()) - { - _writer.WriteIndentedLine("configure(descriptor);"); - } - - _writer.WriteIndentedLine("};"); - } - - _writer.WriteIndentedLine("});"); - } - - _writer.WriteIndentedLine("sb.ContextData.Add(typeKey, null);"); - } - - _writer.WriteIndentedLine("}"); - _writer.WriteLine(); - - _writer.WriteIndentedLine("if (!sb.ContextData.TryGetValue(hooksKey, out var value))"); - _writer.WriteIndentedLine("{"); - - using (_writer.IncreaseIndent()) - { - _writer.WriteIndentedLine( - "value = new System.Collections.Generic.List>>();"); - _writer.WriteIndentedLine("sb.ContextData.Add(hooksKey, value);"); - } - - _writer.WriteIndentedLine("}"); - _writer.WriteLine(); - _writer.WriteIndentedLine( - "((System.Collections.Generic.List>>)value!)" + - ".Add(initialize);"); - } - - _writer.WriteIndentedLine("});"); - } - - _writer.WriteIndentedLine("}"); - } - - public void WriteRegisterSelector(string typeName) - => _writer.WriteIndentedLine("builder.AddSelector();", typeName); - - public void WriteRegisterSelector(string typeName, string interfaceTypeName) - => _writer.WriteIndentedLine("builder.AddSelector();", interfaceTypeName, typeName); - - public override string ToString() - => _sb.ToString(); - - public SourceText ToSourceText() - => SourceText.From(ToString(), Encoding.UTF8); - - public void Dispose() - { - if (_disposed) - { - return; - } - - StringBuilderPool.Return(_sb); - _sb = default!; - _writer = default!; - _disposed = true; - } -} diff --git a/src/Fluss.Regen/Fluss.Regen/Generators/SelectorSyntaxGenerator.cs b/src/Fluss.Regen/Fluss.Regen/Generators/SelectorSyntaxGenerator.cs deleted file mode 100644 index cabc692..0000000 --- a/src/Fluss.Regen/Fluss.Regen/Generators/SelectorSyntaxGenerator.cs +++ /dev/null @@ -1,194 +0,0 @@ -using System; -using System.Linq; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using Fluss.Regen.Helpers; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Text; - -namespace Fluss.Regen.Generators; - -public sealed class SelectorSyntaxGenerator : IDisposable -{ - private StringBuilder _sb; - private CodeWriter _writer; - private bool _disposed; - - public SelectorSyntaxGenerator() - { - _sb = StringBuilderPool.Get(); - _writer = new CodeWriter(_sb); - } - - public void WriteHeader() - { - _writer.WriteFileHeader(); - _writer.WriteIndentedLine("using Microsoft.Extensions.DependencyInjection;"); - _writer.WriteLine(); - _writer.WriteIndentedLine("namespace {0}", "Fluss"); - _writer.WriteIndentedLine("{"); - _writer.IncreaseIndent(); - } - - public void WriteInterfaceHeader() - { - _writer.WriteIndentedLine("public partial interface IUnitOfWork"); - _writer.WriteIndentedLine("{"); - _writer.IncreaseIndent(); - } - - public void WriteClassHeader() - { - _writer.WriteIndentedLine("public static class UnitOfWorkSelectors"); - _writer.WriteIndentedLine("{"); - _writer.IncreaseIndent(); - } - - public void WriteEndNamespace() - { - _writer.DecreaseIndent(); - _writer.WriteIndentedLine("}"); - _writer.DecreaseIndent(); - _writer.WriteIndentedLine("}"); - _writer.WriteLine(); - } - - public void WriteSelectorInterface( - string name, - bool isPublic, - ITypeSymbol key, - ITypeSymbol value) - { - _writer.WriteIndentedLine( - "{0} interface {1}", - isPublic - ? "public" - : "internal", - name); - _writer.IncreaseIndent(); - - _writer.WriteIndentedLine( - ": global::GreenDonut.ISelector<{0}, {1}[]>", - key.ToFullyQualified(), - value.ToFullyQualified()); - - _writer.DecreaseIndent(); - _writer.WriteIndentedLine("{"); - _writer.WriteIndentedLine("}"); - _writer.WriteLine(); - } - - public void WriteBeginSelectorMethod( - string name, - string interfaceName, - bool isPublic, - ITypeSymbol key, - ITypeSymbol value) - { - _writer.WriteIndentedLine( - "{0} sealed class {1}", - isPublic - ? "public" - : "internal", - name); - _writer.IncreaseIndent(); - - _writer.WriteIndentedLine( - ": global::GreenDonut.CacheSelector<{0}, {1}>", - key.ToFullyQualified(), - value.ToFullyQualified()); - - _writer.WriteIndentedLine(", {0}", interfaceName); - _writer.DecreaseIndent(); - _writer.WriteIndentedLine("{"); - _writer.IncreaseIndent(); - } - - public void WriteEndSelectorClass() - { - _writer.DecreaseIndent(); - _writer.WriteIndentedLine("}"); - _writer.WriteLine(); - } - - public void WriteStartSelectorSelectInterfaceMethod(string methodName, - ITypeSymbol value) - { - _writer.WriteIndentedLine( - "public global::{0}<{1}> Select{2}(", - typeof(ValueTask).FullName, - value.ToFullyQualified(), - methodName); - _writer.IncreaseIndent(); - } - - public void WriteStartSelectorSelectMethod(string methodName, - ITypeSymbol value) - { - _writer.WriteIndentedLine( - "public static async global::{0}<{1}> Select{2}(this global::Fluss.IUnitOfWork unitOfWork, ", - typeof(ValueTask).FullName, - value.ToFullyQualified(), - methodName); - _writer.IncreaseIndent(); - } - - public void WriteSelectorMethodParameter(ITypeSymbol parameterType, string parameterName, bool isLast) - { - _writer.WriteIndentedLine( - "{0} {1}{2}", - parameterType.ToFullyQualified(), - parameterName, - isLast ? "" : "," - ); - } - - public void WriteSelectorMethodCall(string containingType, string methodName, bool isAsync) - { - _writer.DecreaseIndent(); - _writer.WriteIndentedLine(")"); - _writer.WriteIndentedLine("{"); - _writer.IncreaseIndent(); - _writer.WriteIndentedLine("return {0}global::{1}.{2}(", isAsync ? "await " : "", containingType, methodName); - _writer.IncreaseIndent(); - } - - public void WriteSelectorMethodCallParameter(string parameterName, bool isLast) - { - _writer.WriteIndentedLine("{0}{1}", parameterName, isLast ? "" : ","); - } - - public void WriteSelectorInterfaceEnd() - { - _writer.DecreaseIndent(); - _writer.WriteIndentedLine(");"); - } - - public void WriteSelectorMethodEnd(bool isAsync) - { - _writer.DecreaseIndent(); - _writer.WriteIndentedLine("){0};", isAsync ? ".ConfigureAwait(false)" : ""); - _writer.DecreaseIndent(); - _writer.WriteIndentedLine("}"); - } - - public override string ToString() - => _sb.ToString(); - - public SourceText ToSourceText() - => SourceText.From(ToString(), Encoding.UTF8); - - public void Dispose() - { - if (_disposed) - { - return; - } - - StringBuilderPool.Return(_sb); - _sb = default!; - _writer = default!; - _disposed = true; - } -} \ No newline at end of file diff --git a/src/Fluss.Regen/Fluss.Regen/SampleIncrementalSourceGenerator.cs b/src/Fluss.Regen/Fluss.Regen/SampleIncrementalSourceGenerator.cs deleted file mode 100644 index 349a9d9..0000000 --- a/src/Fluss.Regen/Fluss.Regen/SampleIncrementalSourceGenerator.cs +++ /dev/null @@ -1,125 +0,0 @@ -using System.Collections.Immutable; -using System.Linq; -using System.Text; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.Text; - - -namespace Fluss.Regen; - -/// -/// A sample source generator that creates a custom report based on class properties. The target class should be annotated with the 'Generators.ReportAttribute' attribute. -/// When using the source code as a baseline, an incremental source generator is preferable because it reduces the performance overhead. -/// -public class SampleIncrementalSourceGenerator : IIncrementalGenerator -{ - private const string Namespace = "Fluss.Regen"; - private const string AttributeName = "SelectorAttribute"; - - private const string AttributeSourceCode = $@"// - -namespace {Namespace} -{{ - [System.AttributeUsage(System.AttributeTargets.Method)] - public class {AttributeName} : System.Attribute - {{ - }} -}}"; - - public void Initialize(IncrementalGeneratorInitializationContext context) - { - - // Add the marker attribute to the compilation. - context.RegisterPostInitializationOutput(ctx => ctx.AddSource( - "SelectorAttribute.g.cs", - SourceText.From(AttributeSourceCode, Encoding.UTF8))); - - // Filter classes annotated with the [Report] attribute. Only filtered Syntax Nodes can trigger code generation. - var provider = context.SyntaxProvider - .CreateSyntaxProvider( - (s, _) => s is MethodDeclarationSyntax, - (ctx, _) => GetMethodDeclarationForSourceGen(ctx)) - .Where(t => t.reportAttributeFound) - .Select((t, _) => t.Item1); - - // Generate the source code. - context.RegisterSourceOutput(context.CompilationProvider.Combine(provider.Collect()), - (ctx, t) => GenerateCode(ctx, t.Left, t.Right)); - } - - /// - /// Checks whether the Node is annotated with the [Report] attribute and maps syntax context to the specific node type (MethodDeclarationSyntax). - /// - /// Syntax context, based on CreateSyntaxProvider predicate - /// The specific cast and whether the attribute was found. - private static (MethodDeclarationSyntax, bool reportAttributeFound) GetMethodDeclarationForSourceGen( - GeneratorSyntaxContext context) - { - var methodDeclarationSyntax = (MethodDeclarationSyntax)context.Node; - - // Go through all attributes of the class. - foreach (var attributeListSyntax in methodDeclarationSyntax.AttributeLists) - foreach (var attributeSyntax in attributeListSyntax.Attributes) - { - if (context.SemanticModel.GetSymbolInfo(attributeSyntax).Symbol is not IMethodSymbol attributeSymbol) - continue; // if we can't get the symbol, ignore it - - var attributeName = attributeSymbol.ContainingType.ToDisplayString(); - - // Check the full name of the [Report] attribute. - if (attributeName == $"{Namespace}.{AttributeName}") - return (methodDeclarationSyntax, true); - } - - return (methodDeclarationSyntax, false); - } - - /// - /// Generate code action. - /// It will be executed on specific nodes (MethodDeclarationSyntax annotated with the [Report] attribute) changed by the user. - /// - /// Source generation context used to add source files. - /// Compilation used to provide access to the Semantic Model. - /// Nodes annotated with the [Report] attribute that trigger the generate action. - private void GenerateCode(SourceProductionContext context, Compilation compilation, - ImmutableArray methodDeclarations) - { - // Go through all filtered class declarations. - foreach (var methodDeclarationSyntax in methodDeclarations) - { - // We need to get semantic model of the class to retrieve metadata. - var semanticModel = compilation.GetSemanticModel(methodDeclarationSyntax.SyntaxTree); - - // Symbols allow us to get the compile-time information. - if (semanticModel.GetDeclaredSymbol(methodDeclarationSyntax) is not IMethodSymbol methodSymbol) - continue; - - var namespaceName = methodSymbol.ContainingNamespace.ToDisplayString(); - - // 'Identifier' means the token of the node. Get class name from the syntax node. - var methodName = methodDeclarationSyntax.Identifier.Text; - - var parameters = methodDeclarationSyntax.ParameterList; - - // Build up the source code - var code = $@"// - -using System; -using System.Collections.Generic; - -namespace {namespaceName}; - -public class {methodName}Selector -{{ - public Select({string.Join(", ", parameters.Parameters.Select(p => "global::" + p.Type!.ToFullString() + " " + p.Identifier.Text))}) - {{ - }} -}} -"; - - // Add the source code to the compilation. - context.AddSource($"{methodName}.g.cs", SourceText.From(code, Encoding.UTF8)); - } - } -} \ No newline at end of file diff --git a/src/Fluss.Regen/Generators/SelectorSyntaxGenerator.cs b/src/Fluss.Regen/Generators/SelectorSyntaxGenerator.cs new file mode 100644 index 0000000..965fa74 --- /dev/null +++ b/src/Fluss.Regen/Generators/SelectorSyntaxGenerator.cs @@ -0,0 +1,181 @@ +using System; +using System.Text; +using System.Threading.Tasks; +using Fluss.Regen.Helpers; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Text; + +namespace Fluss.Regen.Generators; + +public sealed class SelectorSyntaxGenerator : IDisposable +{ + private StringBuilder _sb; + private CodeWriter _writer; + private bool _disposed; + + public SelectorSyntaxGenerator() + { + _sb = StringBuilderPool.Get(); + _writer = new CodeWriter(_sb); + } + + public void WriteHeader() + { + _writer.WriteFileHeader(); + _writer.WriteLine(); + _writer.WriteIndentedLine("namespace {0}", "Fluss"); + _writer.WriteIndentedLine("{"); + _writer.IncreaseIndent(); + } + + public void WriteClassHeader() + { + _writer.WriteIndentedLine("public static class UnitOfWorkSelectors"); + _writer.WriteIndentedLine("{"); + _writer.IncreaseIndent(); + _writer.WriteIndentedLine("private static global::Microsoft.Extensions.Caching.Memory.MemoryCache _cache = new (new global::Microsoft.Extensions.Caching.Memory.MemoryCacheOptions { SizeLimit = 1024 });"); + } + + public void WriteEndNamespace() + { + _writer.WriteIndentedLine(""" + private record CacheEntryValue(object? Value, global::System.Collections.Generic.IReadOnlyList? EventListeners); + private static async ValueTask MatchesEventListenerState(IUnitOfWork unitOfWork, CacheEntryValue value) { + foreach (var eventListenerData in value.EventListeners ?? global::System.Array.Empty()) { + if (!(await eventListenerData.IsStillUpToDate(unitOfWork))) { + return false; + } + } + + return true; + } + """); + + _writer.DecreaseIndent(); + _writer.WriteIndentedLine("}"); + _writer.DecreaseIndent(); + _writer.WriteIndentedLine("}"); + _writer.WriteLine(); + } + + public void WriteMethodSignatureStart(string methodName, ITypeSymbol returnType, bool noParameters) + { + _writer.WriteLine(); + _writer.WriteIndentedLine( + "public static async global::{0}<{1}> Select{2}(this global::Fluss.IUnitOfWork unitOfWork{3}", + typeof(ValueTask).FullName, + returnType.ToFullyQualified(), + methodName, + noParameters ? "" : ", "); + _writer.IncreaseIndent(); + } + + public void WriteMethodSignatureParameter(ITypeSymbol parameterType, string parameterName, bool isLast) + { + _writer.WriteIndentedLine( + "{0} {1}{2}", + parameterType.ToFullyQualified(), + parameterName, + isLast ? "" : "," + ); + } + + public void WriteMethodSignatureEnd() + { + _writer.DecreaseIndent(); + _writer.WriteIndentedLine(")"); + _writer.WriteIndentedLine("{"); + _writer.IncreaseIndent(); + } + + public void WriteRecordingUnitOfWork() + { + _writer.WriteIndentedLine("var recordingUnitOfWork = new global::Fluss.UnitOfWorkRecordingProxy(unitOfWork);"); + } + + public void WriteKeyStart(string containingType, string methodName, bool noParameters) + { + _writer.WriteIndentedLine("var key = ("); + _writer.IncreaseIndent(); + _writer.WriteIndentedLine("\"{0}.{1}\"{2}", containingType, methodName, noParameters ? "" : ","); + } + + public void WriteKeyParameter(string parameterName, bool isLast) + { + _writer.WriteIndentedLine("{0}{1}", parameterName, isLast ? "" : ","); + } + + public void WriteKeyEnd() + { + _writer.DecreaseIndent(); + _writer.WriteIndentedLine(");"); + _writer.WriteLine(); + } + + public void WriteMethodCacheHit(ITypeSymbol returnType) + { + _writer.WriteIndented("if (_cache.TryGetValue(key, out var result) && result is CacheEntryValue entryValue && await MatchesEventListenerState(unitOfWork, entryValue)) "); + using (_writer.WriteBraces()) + { + _writer.WriteIndentedLine("return ({0})entryValue.Value;", returnType.ToFullyQualified()); + } + _writer.WriteLine(); + } + + public void WriteMethodCall(string containingType, string methodName, bool isAsync) + { + _writer.WriteIndentedLine("result = {0}global::{1}.{2}(", isAsync ? "await " : "", containingType, methodName); + _writer.IncreaseIndent(); + } + + public void WriteMethodCallParameter(string parameterName, bool isLast) + { + _writer.WriteIndentedLine("{0}{1}", parameterName, isLast ? "" : ","); + } + + public void WriteMethodCallEnd(bool isAsync) + { + _writer.DecreaseIndent(); + _writer.WriteIndentedLine("){0};", isAsync ? ".ConfigureAwait(false)" : ""); + _writer.WriteLine(); + } + + public void WriteMethodCacheMiss(ITypeSymbol returnType) + { + _writer.WriteIndented("using (var entry = _cache.CreateEntry(key)) "); + + using (_writer.WriteBraces()) + { + _writer.WriteIndentedLine("entry.Value = new CacheEntryValue(result, recordingUnitOfWork.GetRecordedListeners());"); + _writer.WriteIndentedLine("entry.Size = 1;"); + } + + _writer.WriteLine(); + _writer.WriteIndentedLine("return ({0})result;", returnType.ToFullyQualified()); + } + + public void WriteMethodEnd() + { + _writer.DecreaseIndent(); + _writer.WriteIndentedLine("}"); + } + + public override string ToString() + => _sb.ToString(); + + public SourceText ToSourceText() + => SourceText.From(ToString(), Encoding.UTF8); + + public void Dispose() + { + if (_disposed) + { + return; + } + + StringBuilderPool.Return(_sb); + _sb = default!; + _writer = default!; + _disposed = true; + } +} diff --git a/src/Fluss.Regen/Fluss.Regen/Generators/StringBuilderPool.cs b/src/Fluss.Regen/Generators/StringBuilderPool.cs similarity index 100% rename from src/Fluss.Regen/Fluss.Regen/Generators/StringBuilderPool.cs rename to src/Fluss.Regen/Generators/StringBuilderPool.cs diff --git a/src/Fluss.Regen/Fluss.Regen/Helpers/CodeWriter.cs b/src/Fluss.Regen/Helpers/CodeWriter.cs similarity index 99% rename from src/Fluss.Regen/Fluss.Regen/Helpers/CodeWriter.cs rename to src/Fluss.Regen/Helpers/CodeWriter.cs index 00b9e17..1a75fb0 100644 --- a/src/Fluss.Regen/Fluss.Regen/Helpers/CodeWriter.cs +++ b/src/Fluss.Regen/Helpers/CodeWriter.cs @@ -73,7 +73,7 @@ public void WriteIndentedLine(string format, params object?[] args) WriteLine(); } - + public void WriteIndented(string format, params object?[] args) { WriteIndent(); @@ -115,10 +115,10 @@ public IDisposable WriteBraces() return new Block(() => { - WriteLine(); indent.Dispose(); WriteIndent(); WriteRightBrace(); + WriteLine(); }); } diff --git a/src/Fluss.Regen/Fluss.Regen/Helpers/CodeWriterExtensions.cs b/src/Fluss.Regen/Helpers/CodeWriterExtensions.cs similarity index 51% rename from src/Fluss.Regen/Fluss.Regen/Helpers/CodeWriterExtensions.cs rename to src/Fluss.Regen/Helpers/CodeWriterExtensions.cs index ef01fe1..feb2ccc 100644 --- a/src/Fluss.Regen/Fluss.Regen/Helpers/CodeWriterExtensions.cs +++ b/src/Fluss.Regen/Helpers/CodeWriterExtensions.cs @@ -4,25 +4,6 @@ namespace Fluss.Regen.Helpers; public static class CodeWriterExtensions { - public static void WriteGeneratedAttribute(this CodeWriter writer) - { - if (writer is null) - { - throw new ArgumentNullException(nameof(writer)); - } - -#if DEBUG - writer.WriteIndentedLine( - "[global::System.CodeDom.Compiler.GeneratedCode(" + - "\"HotChocolate\", \"11.0.0\")]"); -#else - var version = typeof(CodeWriter).Assembly.GetName().Version!.ToString(); - writer.WriteIndentedLine( - "[global::System.CodeDom.Compiler.GeneratedCode(" + - $"\"HotChocolate\", \"{version}\")]"); -#endif - } - public static void WriteFileHeader(this CodeWriter writer) { if (writer is null) @@ -30,7 +11,7 @@ public static void WriteFileHeader(this CodeWriter writer) throw new ArgumentNullException(nameof(writer)); } - writer.WriteIndentedLine("// "); + writer.WriteComment(""); writer.WriteLine(); writer.WriteIndentedLine("#nullable enable"); writer.WriteLine(); @@ -40,6 +21,7 @@ public static void WriteFileHeader(this CodeWriter writer) public static CodeWriter WriteComment(this CodeWriter writer, string comment) { + writer.WriteIndent(); writer.Write("// "); writer.WriteLine(comment); return writer; diff --git a/src/Fluss.Regen/Fluss.Regen/Helpers/SymbolExtensions.cs b/src/Fluss.Regen/Helpers/SymbolExtensions.cs similarity index 100% rename from src/Fluss.Regen/Fluss.Regen/Helpers/SymbolExtensions.cs rename to src/Fluss.Regen/Helpers/SymbolExtensions.cs diff --git a/src/Fluss.Regen/Fluss.Regen/Inspectors/ISyntaxInfo.cs b/src/Fluss.Regen/Inspectors/ISyntaxInfo.cs similarity index 100% rename from src/Fluss.Regen/Fluss.Regen/Inspectors/ISyntaxInfo.cs rename to src/Fluss.Regen/Inspectors/ISyntaxInfo.cs diff --git a/src/Fluss.Regen/Fluss.Regen/Inspectors/ISyntaxInspector.cs b/src/Fluss.Regen/Inspectors/ISyntaxInspector.cs similarity index 100% rename from src/Fluss.Regen/Fluss.Regen/Inspectors/ISyntaxInspector.cs rename to src/Fluss.Regen/Inspectors/ISyntaxInspector.cs diff --git a/src/Fluss.Regen/Fluss.Regen/Inspectors/SelectorInfo.cs b/src/Fluss.Regen/Inspectors/SelectorInfo.cs similarity index 79% rename from src/Fluss.Regen/Fluss.Regen/Inspectors/SelectorInfo.cs rename to src/Fluss.Regen/Inspectors/SelectorInfo.cs index 82c50cb..359ebcb 100644 --- a/src/Fluss.Regen/Fluss.Regen/Inspectors/SelectorInfo.cs +++ b/src/Fluss.Regen/Inspectors/SelectorInfo.cs @@ -5,15 +5,11 @@ namespace Fluss.Regen.Inspectors; public sealed class SelectorInfo : ISyntaxInfo { - public AttributeSyntax AttributeSyntax { get; } - public IMethodSymbol AttributeSymbol { get; } + private AttributeSyntax AttributeSyntax { get; } public IMethodSymbol MethodSymbol { get; } - public MethodDeclarationSyntax MethodSyntax { get; } + private MethodDeclarationSyntax MethodSyntax { get; } public string Name { get; } - public string InterfaceName { get; } public string Namespace { get; } - public string FullName { get; } - public string InterfaceFullName { get; } public string ContainingType { get; } public SelectorInfo( @@ -24,19 +20,14 @@ MethodDeclarationSyntax methodSyntax ) { AttributeSyntax = attributeSyntax; - AttributeSymbol = attributeSymbol; MethodSymbol = methodSymbol; MethodSyntax = methodSyntax; Name = methodSymbol.Name; - InterfaceName = $"I{Name}"; Namespace = methodSymbol.ContainingNamespace.ToDisplayString(); - FullName = $"{Namespace}.{Name}"; - InterfaceFullName = $"{Namespace}.{InterfaceName}"; - ContainingType = methodSymbol.ContainingType.ToDisplayString(); } - + public bool Equals(SelectorInfo? other) { if (ReferenceEquals(null, other)) @@ -52,7 +43,7 @@ public bool Equals(SelectorInfo? other) return AttributeSyntax.Equals(other.AttributeSyntax) && MethodSyntax.Equals(other.MethodSyntax); } - + public bool Equals(ISyntaxInfo other) { if (ReferenceEquals(null, other)) diff --git a/src/Fluss.Regen/Fluss.Regen/Inspectors/SelectorInspector.cs b/src/Fluss.Regen/Inspectors/SelectorInspector.cs similarity index 100% rename from src/Fluss.Regen/Fluss.Regen/Inspectors/SelectorInspector.cs rename to src/Fluss.Regen/Inspectors/SelectorInspector.cs diff --git a/src/Fluss.Regen/Fluss.Regen/Inspectors/SyntaxInfoComparer.cs b/src/Fluss.Regen/Inspectors/SyntaxInfoComparer.cs similarity index 100% rename from src/Fluss.Regen/Fluss.Regen/Inspectors/SyntaxInfoComparer.cs rename to src/Fluss.Regen/Inspectors/SyntaxInfoComparer.cs diff --git a/src/Fluss.Regen/Fluss.Regen/Properties/launchSettings.json b/src/Fluss.Regen/Properties/launchSettings.json similarity index 100% rename from src/Fluss.Regen/Fluss.Regen/Properties/launchSettings.json rename to src/Fluss.Regen/Properties/launchSettings.json diff --git a/src/Fluss.Regen/Fluss.Regen/Readme.md b/src/Fluss.Regen/Readme.md similarity index 100% rename from src/Fluss.Regen/Fluss.Regen/Readme.md rename to src/Fluss.Regen/Readme.md diff --git a/src/Fluss.Regen/Fluss.Regen/AutoLoadGenerator.cs b/src/Fluss.Regen/SelectorGenerator.cs similarity index 65% rename from src/Fluss.Regen/Fluss.Regen/AutoLoadGenerator.cs rename to src/Fluss.Regen/SelectorGenerator.cs index 243021a..4ad6529 100644 --- a/src/Fluss.Regen/Fluss.Regen/AutoLoadGenerator.cs +++ b/src/Fluss.Regen/SelectorGenerator.cs @@ -6,7 +6,6 @@ using System.Threading.Tasks; using Fluss.Regen.Attributes; using Fluss.Regen.Generators; -using Fluss.Regen.Helpers; using Fluss.Regen.Inspectors; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -16,7 +15,7 @@ namespace Fluss.Regen; [Generator] -public class AutoLoadGenerator : IIncrementalGenerator +public class SelectorGenerator : IIncrementalGenerator { private static readonly ISyntaxInspector[] Inspectors = [ @@ -28,7 +27,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) context.RegisterPostInitializationOutput(ctx => ctx.AddSource( "SelectorAttribute.g.cs", SourceText.From(SelectorAttribute.AttributeSourceCode, Encoding.UTF8))); - + var modulesAndTypes = context.SyntaxProvider .CreateSyntaxProvider( predicate: static (s, _) => IsRelevant(s), @@ -37,7 +36,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) .WithComparer(SyntaxInfoComparer.Default); var valueProvider = context.CompilationProvider.Combine(modulesAndTypes.Collect()); - + context.RegisterSourceOutput( valueProvider, static (context, source) => Execute(context, source.Right)); @@ -82,11 +81,10 @@ private static void Execute( } var syntaxInfoList = syntaxInfos.ToList(); - // WriteSelectorInterfaces(context, syntaxInfoList); WriteSelectorMethods(context, syntaxInfoList); } - private static void WriteSelectorInterfaces(SourceProductionContext context, List syntaxInfos) + private static void WriteSelectorMethods(SourceProductionContext context, List syntaxInfos) { var selectors = new List(); @@ -102,68 +100,74 @@ private static void WriteSelectorInterfaces(SourceProductionContext context, Lis using var generator = new SelectorSyntaxGenerator(); generator.WriteHeader(); - generator.WriteInterfaceHeader(); - + generator.WriteClassHeader(); + foreach (var selector in selectors) { - generator.WriteStartSelectorSelectInterfaceMethod(selector.Name, ExtractValueType(selector.MethodSymbol.ReturnType)); + var parametersWithoutUnitOfWork = new List(); - for (var index = 0; index < selector.MethodSymbol.Parameters.Length; index++) + foreach (var parameter in selector.MethodSymbol.Parameters) { - var parameter = selector.MethodSymbol.Parameters[index]; - generator.WriteSelectorMethodParameter(parameter.Type, parameter.Name, index == selector.MethodSymbol.Parameters.Length - 1); + if (ToTypeNameNoGenerics(parameter.Type) != "Fluss.IUnitOfWork") + { + parametersWithoutUnitOfWork.Add(parameter); + } } - generator.WriteSelectorInterfaceEnd(); - } - - generator.WriteEndNamespace(); + var isAsync = ToTypeNameNoGenerics(selector.MethodSymbol.ReturnType) == typeof(ValueTask).FullName || + ToTypeNameNoGenerics(selector.MethodSymbol.ReturnType) == typeof(Task).FullName; + var hasUnitOfWorkParameter = selector.MethodSymbol.Parameters.Length != parametersWithoutUnitOfWork.Count; - context.AddSource("SelectorInterfaces.g.cs", generator.ToSourceText()); - } + var returnType = ExtractValueType(selector.MethodSymbol.ReturnType); - - private static void WriteSelectorMethods(SourceProductionContext context, List syntaxInfos) - { - var selectors = new List(); + generator.WriteMethodSignatureStart(selector.Name, returnType, parametersWithoutUnitOfWork.Count == 0); - foreach (var syntaxInfo in syntaxInfos) - { - if (syntaxInfo is not SelectorInfo selector) + for (var index = 0; index < parametersWithoutUnitOfWork.Count; index++) { - continue; + var parameter = parametersWithoutUnitOfWork[index]; + generator.WriteMethodSignatureParameter(parameter.Type, parameter.Name, parametersWithoutUnitOfWork.Count - 1 == index); } - selectors.Add(selector); - } + generator.WriteMethodSignatureEnd(); - using var generator = new SelectorSyntaxGenerator(); - generator.WriteHeader(); - generator.WriteClassHeader(); - - foreach (var selector in selectors) - { - var isAsync = ToTypeNameNoGenerics(selector.MethodSymbol.ReturnType) == typeof(ValueTask).FullName || - ToTypeNameNoGenerics(selector.MethodSymbol.ReturnType) == typeof(Task).FullName; - - generator.WriteStartSelectorSelectMethod(selector.Name, ExtractValueType(selector.MethodSymbol.ReturnType)); + if (hasUnitOfWorkParameter) + { + generator.WriteRecordingUnitOfWork(); + } - for (var index = 0; index < selector.MethodSymbol.Parameters.Length; index++) + generator.WriteKeyStart(selector.ContainingType, selector.Name, parametersWithoutUnitOfWork.Count == 0); + + for (var index = 0; index < parametersWithoutUnitOfWork.Count; index++) { - var parameter = selector.MethodSymbol.Parameters[index]; - generator.WriteSelectorMethodParameter(parameter.Type, parameter.Name, index == selector.MethodSymbol.Parameters.Length - 1); + var parameter = parametersWithoutUnitOfWork[index]; + generator.WriteKeyParameter(parameter.Name, index == parametersWithoutUnitOfWork.Count - 1); } - generator.WriteSelectorMethodCall(selector.ContainingType, selector.Name, isAsync); + generator.WriteKeyEnd(); + generator.WriteMethodCacheHit(returnType); + + generator.WriteMethodCall(selector.ContainingType, selector.Name, isAsync); for (var index = 0; index < selector.MethodSymbol.Parameters.Length; index++) { var parameter = selector.MethodSymbol.Parameters[index]; - generator.WriteSelectorMethodCallParameter(parameter.Name, index == selector.MethodSymbol.Parameters.Length - 1); + + if (ToTypeNameNoGenerics(parameter.Type) == "Fluss.IUnitOfWork") + { + generator.WriteMethodCallParameter("recordingUnitOfWork", index == selector.MethodSymbol.Parameters.Length - 1); + } + else + { + generator.WriteMethodCallParameter(parameter.Name, index == selector.MethodSymbol.Parameters.Length - 1); + } } - - generator.WriteSelectorMethodEnd(isAsync); + + generator.WriteMethodCallEnd(isAsync); + + generator.WriteMethodCacheMiss(returnType); + + generator.WriteMethodEnd(); } - + generator.WriteEndNamespace(); context.AddSource("Selectors.g.cs", generator.ToSourceText()); @@ -176,10 +180,10 @@ private static ITypeSymbol ExtractValueType(ITypeSymbol returnType) { return namedTypeSymbol.TypeArguments[0]; } - + return returnType; } private static string ToTypeNameNoGenerics(ITypeSymbol typeSymbol) => $"{typeSymbol.ContainingNamespace}.{typeSymbol.Name}"; -} \ No newline at end of file +} diff --git a/src/Fluss.Sample/Fluss.Sample.csproj b/src/Fluss.Sample/Fluss.Sample.csproj deleted file mode 100644 index 98926e0..0000000 --- a/src/Fluss.Sample/Fluss.Sample.csproj +++ /dev/null @@ -1,21 +0,0 @@ - - - - Exe - net8.0 - enable - enable - - - - - - false - Analyzer - - - - - - - diff --git a/src/Fluss.Sample/Program.cs b/src/Fluss.Sample/Program.cs deleted file mode 100644 index 3bef9a7..0000000 --- a/src/Fluss.Sample/Program.cs +++ /dev/null @@ -1,19 +0,0 @@ -// See https://aka.ms/new-console-template for more information - -using Fluss; -using Fluss.Authentication; -using Microsoft.Extensions.DependencyInjection; - -Console.WriteLine("Hello, World!"); - -var sc = new ServiceCollection(); - -var sp = sc.AddEventSourcing().ProvideUserIdFrom(_ => Guid.Empty).BuildServiceProvider(); - -var unitOfWork = sp.GetRequiredService(); - -var version = await unitOfWork.ConsistentVersion(); - -var result = await unitOfWork.SelectAdd(1, 2); - -Console.WriteLine($"{version} {result}"); \ No newline at end of file diff --git a/src/Fluss.Sample/Test.cs b/src/Fluss.Sample/Test.cs deleted file mode 100644 index 3ed0394..0000000 --- a/src/Fluss.Sample/Test.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Fluss.Regen; - -namespace Fluss.Sample; - -public class Test -{ - [Selector] - public static int Add(int a, int b) - { - return a + b; - } -} \ No newline at end of file diff --git a/src/Fluss.UnitTest/Core/Authentication/ArbitraryUserUnitOfWorkExtensionTest.cs b/src/Fluss.UnitTest/Core/Authentication/ArbitraryUserUnitOfWorkExtensionTest.cs index 725a3c0..19b53ed 100644 --- a/src/Fluss.UnitTest/Core/Authentication/ArbitraryUserUnitOfWorkExtensionTest.cs +++ b/src/Fluss.UnitTest/Core/Authentication/ArbitraryUserUnitOfWorkExtensionTest.cs @@ -10,7 +10,7 @@ public class ArbitraryUserUnitOfWorkExtensionTest public async Task CanCreateUnitOfWorkWithArbitraryGuid() { var guid = Guid.NewGuid(); - + var serviceCollection = new ServiceCollection(); serviceCollection .AddEventSourcing(false) @@ -19,21 +19,21 @@ public async Task CanCreateUnitOfWorkWithArbitraryGuid() var serviceProvider = serviceCollection.BuildServiceProvider(); // ReSharper disable once InvokeAsExtensionMethod - var unitOfWork = ArbitraryUserUnitOfWorkExtension.GetUserUnitOfWork(serviceProvider, guid); + var unitOfWork = (Fluss.UnitOfWork)ArbitraryUserUnitOfWorkExtension.GetUserUnitOfWork(serviceProvider, guid); await unitOfWork.Publish(new TestEvent()); - await ((Fluss.UnitOfWork) unitOfWork).CommitInternal(); + await unitOfWork.CommitInternal(); var inMemoryEventRepository = serviceProvider.GetRequiredService(); var events = await inMemoryEventRepository.GetEvents(-1, 0); - + Assert.Equal(guid, events[0].ToArray()[0].By); } - + [Fact] public async Task CanCreateUnitOfWorkFactoryWithArbitraryGuid() { var guid = Guid.NewGuid(); - + var serviceCollection = new ServiceCollection(); serviceCollection .AddEventSourcing(false) @@ -48,15 +48,15 @@ await unitOfWorkFactory.Commit(async work => { await work.Publish(new TestEvent()); }); - + var inMemoryEventRepository = serviceProvider.GetRequiredService(); var events = await inMemoryEventRepository.GetEvents(-1, 0); - + Assert.Equal(guid, events[0].ToArray()[0].By); } - - private class TestEvent : Event {} - + + private class TestEvent : Event { } + private class AllowAllPolicy : Policy { public ValueTask AuthenticateEvent(EventEnvelope envelope, IAuthContext authContext) diff --git a/src/Fluss.UnitTest/Core/Extensions/ValueTaskTest.cs b/src/Fluss.UnitTest/Core/Extensions/ValueTaskTest.cs index c8a0d9c..6c819a9 100644 --- a/src/Fluss.UnitTest/Core/Extensions/ValueTaskTest.cs +++ b/src/Fluss.UnitTest/Core/Extensions/ValueTaskTest.cs @@ -5,25 +5,25 @@ namespace Fluss.UnitTest.Core.Extensions; public class ValueTaskTest { [Fact] - public async void AnyReturnsFalse() + public async Task AnyReturnsFalse() { Assert.False(await new[] { False() }.AnyAsync()); } [Fact] - public async void AnyReturnsTrue() + public async Task AnyReturnsTrue() { Assert.True(await new[] { False(), True() }.AnyAsync()); } [Fact] - public async void AllReturnsFalse() + public async Task AllReturnsFalse() { Assert.False(await new[] { False(), True() }.AllAsync()); } [Fact] - public async void AllReturnsTrue() + public async Task AllReturnsTrue() { Assert.True(await new[] { True(), True() }.AllAsync()); } diff --git a/src/Fluss.UnitTest/Core/SideEffects/DispatcherTest.cs b/src/Fluss.UnitTest/Core/SideEffects/DispatcherTest.cs index be6cbef..a1cfd02 100644 --- a/src/Fluss.UnitTest/Core/SideEffects/DispatcherTest.cs +++ b/src/Fluss.UnitTest/Core/SideEffects/DispatcherTest.cs @@ -40,7 +40,7 @@ await serviceProvider.GetRequiredService().Commit(async unitO var testSideEffect = serviceProvider.GetRequiredService(); await WaitUntilTrue(() => testSideEffect.DidTrigger, TimeSpan.FromMilliseconds(100)); - + Assert.True(testSideEffect.DidTrigger); await dispatcher.StopAsync(CancellationToken.None); @@ -118,7 +118,7 @@ public Task> HandleAsync(TestTransientEvent @event, return Task.FromResult>(Array.Empty()); } } - + [Fact] public async Task PublishesNewEventsReturnedBySideEffect() { @@ -147,18 +147,18 @@ await serviceProvider.GetRequiredService().Commit(async unitO }); var repository = serviceProvider.GetRequiredService(); - + await WaitUntilTrue(async () => await repository.GetLatestVersion() >= 1, TimeSpan.FromMilliseconds(100)); var newEvent = (await repository.GetEvents(0, 1).ToFlatEventList())[0]; - + Assert.True(newEvent.Event is TestReturnedEvent); await dispatcher.StopAsync(CancellationToken.None); } - - private class TestTriggerEvent : Event {} - private class TestReturnedEvent : Event {} + + private class TestTriggerEvent : Event { } + private class TestReturnedEvent : Event { } private class TestReturningSideEffect : SideEffect { diff --git a/src/Fluss.UnitTest/Core/UnitOfWork/UnitOfWorkAndAuthorizationTest.cs b/src/Fluss.UnitTest/Core/UnitOfWork/UnitOfWorkAndAuthorizationTest.cs index 1d317f3..7632458 100644 --- a/src/Fluss.UnitTest/Core/UnitOfWork/UnitOfWorkAndAuthorizationTest.cs +++ b/src/Fluss.UnitTest/Core/UnitOfWork/UnitOfWorkAndAuthorizationTest.cs @@ -53,7 +53,7 @@ public async ValueTask AuthenticateReadModel(IReadModel readModel, IAuthCo public async Task AnEmptyPolicyDoesNotAllowAnything() { Policy emptyPolicy = new EmptyPolicy(); - + Assert.False(await emptyPolicy.AuthenticateEvent(null!, null!)); Assert.False(await emptyPolicy.AuthenticateReadModel(null!, null!)); } diff --git a/src/Fluss.UnitTest/Core/Validation/RootValidatorTests.cs b/src/Fluss.UnitTest/Core/Validation/RootValidatorTests.cs index 6b90815..48f155f 100644 --- a/src/Fluss.UnitTest/Core/Validation/RootValidatorTests.cs +++ b/src/Fluss.UnitTest/Core/Validation/RootValidatorTests.cs @@ -9,17 +9,17 @@ namespace Fluss.UnitTest.Core.Validation; public class RootValidatorTests { private readonly Mock _arbitraryUserUnitOfWorkCacheMock = new(MockBehavior.Strict); - private readonly Mock _unitOfWorkMock = new(MockBehavior.Strict); + private readonly Mock _unitOfWorkMock = new(MockBehavior.Strict); public RootValidatorTests() { _arbitraryUserUnitOfWorkCacheMock.Setup(c => c.GetUserUnitOfWork(It.IsAny())) .Returns(() => _unitOfWorkMock.Object); - + _unitOfWorkMock.Setup(u => u.WithPrefilledVersion(It.IsAny())) .Returns(() => _unitOfWorkMock.Object); } - + [Fact] public async Task ValidatesValidEvent() { @@ -31,7 +31,7 @@ public async Task ValidatesValidEvent() await validator.ValidateEvent(new EventEnvelope { Event = new TestEvent() }); } - + [Fact] public async Task ValidatesInvalidEvent() { @@ -46,7 +46,7 @@ await Assert.ThrowsAsync(async () => await validator.ValidateEvent(new EventEnvelope { Event = new TestEvent() }); }); } - + [Fact] public async Task ValidatesValidAggregate() { @@ -58,7 +58,7 @@ public async Task ValidatesValidAggregate() await validator.ValidateAggregate(new TestAggregate(), new Fluss.UnitOfWork(null!, null!, null!, null!, null!)); } - + [Fact] public async Task ValidatesInvalidAggregate() { @@ -107,7 +107,7 @@ public ValueTask ValidateAsync(TestAggregate aggregateAfterEvent, IUnitOfWork un return ValueTask.CompletedTask; } } - + private class AggregateValidatorAlwaysInvalid : AggregateValidator { public ValueTask ValidateAsync(TestAggregate aggregateAfterEvent, IUnitOfWork unitOfWorkBeforeEvent) diff --git a/src/Fluss.UnitTest/Fluss.UnitTest.csproj b/src/Fluss.UnitTest/Fluss.UnitTest.csproj index 0e81098..4e07e21 100644 --- a/src/Fluss.UnitTest/Fluss.UnitTest.csproj +++ b/src/Fluss.UnitTest/Fluss.UnitTest.csproj @@ -12,8 +12,10 @@ - - + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all @@ -24,8 +26,13 @@ + + + + + diff --git a/src/Fluss.UnitTest/Regen/SelectorGeneratorTests.cs b/src/Fluss.UnitTest/Regen/SelectorGeneratorTests.cs new file mode 100644 index 0000000..4971445 --- /dev/null +++ b/src/Fluss.UnitTest/Regen/SelectorGeneratorTests.cs @@ -0,0 +1,118 @@ +using Fluss.Regen; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; + +namespace Fluss.UnitTest.Regen; + +public class SelectorGeneratorTests +{ + [Fact] + public Task GeneratesForAsyncSelector() + { + var generator = new SelectorGenerator(); + + var driver = CSharpGeneratorDriver.Create(generator); + + var compilation = CSharpCompilation.Create(nameof(SelectorGeneratorTests), + new[] + { + CSharpSyntaxTree.ParseText( + @" +using Fluss.Regen; +using System.Threading.Tasks; + +namespace TestNamespace; + +public class Test +{ + [Selector] + public static async ValueTask Add(int a, int b) { + return a + b; + } + + [Selector] + public static async ValueTask Add2(int a, int b) { + return a + b; + } +}") + }, + new[] + { + MetadataReference.CreateFromFile(typeof(object).Assembly.Location) + }); + + var runResult = driver.RunGenerators(compilation).GetRunResult(); + + return Verify(runResult); + } + + [Fact] + public Task GeneratesForNonAsyncSelector() + { + var generator = new SelectorGenerator(); + + var driver = CSharpGeneratorDriver.Create(generator); + + var compilation = CSharpCompilation.Create(nameof(SelectorGeneratorTests), + new[] + { + CSharpSyntaxTree.ParseText( + @" +using Fluss.Regen; + +namespace TestNamespace; + +public class Test +{ + [Selector] + public static int Add(int a, int b) { + return a + b; + } +}") + }, + new[] + { + MetadataReference.CreateFromFile(typeof(object).Assembly.Location) + }); + + var runResult = driver.RunGenerators(compilation).GetRunResult(); + + return Verify(runResult); + } + + [Fact] + public Task GeneratesForUnitOfWorkSelector() + { + var generator = new SelectorGenerator(); + + var driver = CSharpGeneratorDriver.Create(generator); + + var compilation = CSharpCompilation.Create(nameof(SelectorGeneratorTests), + new[] + { + CSharpSyntaxTree.ParseText( + @" +using Fluss; +using Fluss.Regen; + +namespace TestNamespace; + +public class Test +{ + [Selector] + public static int Add(IUnitOfWork unitOfWork, int a, int b) { + return a + b; + } +}") + }, + new[] + { + MetadataReference.CreateFromFile(typeof(object).Assembly.Location), + MetadataReference.CreateFromFile(typeof(UnitOfWork).Assembly.Location) + }); + + var runResult = driver.RunGenerators(compilation).GetRunResult(); + + return Verify(runResult); + } +} \ No newline at end of file diff --git a/src/Fluss.UnitTest/Regen/Snapshots/SelectorGeneratorTests.GeneratesForAsyncSelector#SelectorAttribute.g.verified.cs b/src/Fluss.UnitTest/Regen/Snapshots/SelectorGeneratorTests.GeneratesForAsyncSelector#SelectorAttribute.g.verified.cs new file mode 100644 index 0000000..6b9ec56 --- /dev/null +++ b/src/Fluss.UnitTest/Regen/Snapshots/SelectorGeneratorTests.GeneratesForAsyncSelector#SelectorAttribute.g.verified.cs @@ -0,0 +1,10 @@ +//HintName: SelectorAttribute.g.cs +// + +namespace Fluss.Regen +{ + [System.AttributeUsage(System.AttributeTargets.Method)] + public class SelectorAttribute : System.Attribute + { + } +} \ No newline at end of file diff --git a/src/Fluss.UnitTest/Regen/Snapshots/SelectorGeneratorTests.GeneratesForAsyncSelector#Selectors.g.verified.cs b/src/Fluss.UnitTest/Regen/Snapshots/SelectorGeneratorTests.GeneratesForAsyncSelector#Selectors.g.verified.cs new file mode 100644 index 0000000..e7f842b --- /dev/null +++ b/src/Fluss.UnitTest/Regen/Snapshots/SelectorGeneratorTests.GeneratesForAsyncSelector#Selectors.g.verified.cs @@ -0,0 +1,82 @@ +//HintName: Selectors.g.cs +// + +#nullable enable + +using System; +using System.Runtime.CompilerServices; + +namespace Fluss +{ + public static class UnitOfWorkSelectors + { + private static global::Microsoft.Extensions.Caching.Memory.MemoryCache _cache = new (new global::Microsoft.Extensions.Caching.Memory.MemoryCacheOptions { SizeLimit = 1024 }); + + public static async global::System.Threading.Tasks.ValueTask SelectAdd(this global::Fluss.IUnitOfWork unitOfWork, + int a, + int b + ) + { + var key = ( + "TestNamespace.Test.Add", + a, + b + ); + + if (_cache.TryGetValue(key, out var result) && result is CacheEntryValue entryValue && await MatchesEventListenerState(unitOfWork, entryValue)) { + return (int)entryValue.Value; + } + + result = await global::TestNamespace.Test.Add( + a, + b + ).ConfigureAwait(false); + + using (var entry = _cache.CreateEntry(key)) { + entry.Value = new CacheEntryValue(result, recordingUnitOfWork.GetRecordedListeners()); + entry.Size = 1; + } + + return (int)result; + } + + public static async global::System.Threading.Tasks.ValueTask SelectAdd2(this global::Fluss.IUnitOfWork unitOfWork, + int a, + int b + ) + { + var key = ( + "TestNamespace.Test.Add2", + a, + b + ); + + if (_cache.TryGetValue(key, out var result) && result is CacheEntryValue entryValue && await MatchesEventListenerState(unitOfWork, entryValue)) { + return (int)entryValue.Value; + } + + result = await global::TestNamespace.Test.Add2( + a, + b + ).ConfigureAwait(false); + + using (var entry = _cache.CreateEntry(key)) { + entry.Value = new CacheEntryValue(result, recordingUnitOfWork.GetRecordedListeners()); + entry.Size = 1; + } + + return (int)result; + } + private record CacheEntryValue(object? Value, global::System.Collections.Generic.IReadOnlyList? EventListeners); +private static async ValueTask MatchesEventListenerState(IUnitOfWork unitOfWork, CacheEntryValue value) { + foreach (var eventListenerData in value.EventListeners ?? global::System.Array.Empty()) { + if (!(await eventListenerData.IsStillUpToDate(unitOfWork))) { + return false; + } + } + + return true; +} + } +} + diff --git a/src/Fluss.UnitTest/Regen/Snapshots/SelectorGeneratorTests.GeneratesForNonAsyncSelector#SelectorAttribute.g.verified.cs b/src/Fluss.UnitTest/Regen/Snapshots/SelectorGeneratorTests.GeneratesForNonAsyncSelector#SelectorAttribute.g.verified.cs new file mode 100644 index 0000000..6b9ec56 --- /dev/null +++ b/src/Fluss.UnitTest/Regen/Snapshots/SelectorGeneratorTests.GeneratesForNonAsyncSelector#SelectorAttribute.g.verified.cs @@ -0,0 +1,10 @@ +//HintName: SelectorAttribute.g.cs +// + +namespace Fluss.Regen +{ + [System.AttributeUsage(System.AttributeTargets.Method)] + public class SelectorAttribute : System.Attribute + { + } +} \ No newline at end of file diff --git a/src/Fluss.UnitTest/Regen/Snapshots/SelectorGeneratorTests.GeneratesForNonAsyncSelector#Selectors.g.verified.cs b/src/Fluss.UnitTest/Regen/Snapshots/SelectorGeneratorTests.GeneratesForNonAsyncSelector#Selectors.g.verified.cs new file mode 100644 index 0000000..8496d3d --- /dev/null +++ b/src/Fluss.UnitTest/Regen/Snapshots/SelectorGeneratorTests.GeneratesForNonAsyncSelector#Selectors.g.verified.cs @@ -0,0 +1,54 @@ +//HintName: Selectors.g.cs +// + +#nullable enable + +using System; +using System.Runtime.CompilerServices; + +namespace Fluss +{ + public static class UnitOfWorkSelectors + { + private static global::Microsoft.Extensions.Caching.Memory.MemoryCache _cache = new (new global::Microsoft.Extensions.Caching.Memory.MemoryCacheOptions { SizeLimit = 1024 }); + + public static async global::System.Threading.Tasks.ValueTask SelectAdd(this global::Fluss.IUnitOfWork unitOfWork, + int a, + int b + ) + { + var key = ( + "TestNamespace.Test.Add", + a, + b + ); + + if (_cache.TryGetValue(key, out var result) && result is CacheEntryValue entryValue && await MatchesEventListenerState(unitOfWork, entryValue)) { + return (int)entryValue.Value; + } + + result = global::TestNamespace.Test.Add( + a, + b + ); + + using (var entry = _cache.CreateEntry(key)) { + entry.Value = new CacheEntryValue(result, recordingUnitOfWork.GetRecordedListeners()); + entry.Size = 1; + } + + return (int)result; + } + private record CacheEntryValue(object? Value, global::System.Collections.Generic.IReadOnlyList? EventListeners); +private static async ValueTask MatchesEventListenerState(IUnitOfWork unitOfWork, CacheEntryValue value) { + foreach (var eventListenerData in value.EventListeners ?? global::System.Array.Empty()) { + if (!(await eventListenerData.IsStillUpToDate(unitOfWork))) { + return false; + } + } + + return true; +} + } +} + diff --git a/src/Fluss.UnitTest/Regen/Snapshots/SelectorGeneratorTests.GeneratesForUnitOfWorkSelector#SelectorAttribute.g.verified.cs b/src/Fluss.UnitTest/Regen/Snapshots/SelectorGeneratorTests.GeneratesForUnitOfWorkSelector#SelectorAttribute.g.verified.cs new file mode 100644 index 0000000..6b9ec56 --- /dev/null +++ b/src/Fluss.UnitTest/Regen/Snapshots/SelectorGeneratorTests.GeneratesForUnitOfWorkSelector#SelectorAttribute.g.verified.cs @@ -0,0 +1,10 @@ +//HintName: SelectorAttribute.g.cs +// + +namespace Fluss.Regen +{ + [System.AttributeUsage(System.AttributeTargets.Method)] + public class SelectorAttribute : System.Attribute + { + } +} \ No newline at end of file diff --git a/src/Fluss.UnitTest/Regen/Snapshots/SelectorGeneratorTests.GeneratesForUnitOfWorkSelector#Selectors.g.verified.cs b/src/Fluss.UnitTest/Regen/Snapshots/SelectorGeneratorTests.GeneratesForUnitOfWorkSelector#Selectors.g.verified.cs new file mode 100644 index 0000000..37b31d9 --- /dev/null +++ b/src/Fluss.UnitTest/Regen/Snapshots/SelectorGeneratorTests.GeneratesForUnitOfWorkSelector#Selectors.g.verified.cs @@ -0,0 +1,56 @@ +//HintName: Selectors.g.cs +// + +#nullable enable + +using System; +using System.Runtime.CompilerServices; + +namespace Fluss +{ + public static class UnitOfWorkSelectors + { + private static global::Microsoft.Extensions.Caching.Memory.MemoryCache _cache = new (new global::Microsoft.Extensions.Caching.Memory.MemoryCacheOptions { SizeLimit = 1024 }); + + public static async global::System.Threading.Tasks.ValueTask SelectAdd(this global::Fluss.IUnitOfWork unitOfWork, + int a, + int b + ) + { + var recordingUnitOfWork = new global::Fluss.UnitOfWorkRecordingProxy(unitOfWork); + var key = ( + "TestNamespace.Test.Add", + a, + b + ); + + if (_cache.TryGetValue(key, out var result) && result is CacheEntryValue entryValue && await MatchesEventListenerState(unitOfWork, entryValue)) { + return (int)entryValue.Value; + } + + result = global::TestNamespace.Test.Add( + recordingUnitOfWork, + a, + b + ); + + using (var entry = _cache.CreateEntry(key)) { + entry.Value = new CacheEntryValue(result, recordingUnitOfWork.GetRecordedListeners()); + entry.Size = 1; + } + + return (int)result; + } + private record CacheEntryValue(object? Value, global::System.Collections.Generic.IReadOnlyList? EventListeners); +private static async ValueTask MatchesEventListenerState(IUnitOfWork unitOfWork, CacheEntryValue value) { + foreach (var eventListenerData in value.EventListeners ?? global::System.Array.Empty()) { + if (!(await eventListenerData.IsStillUpToDate(unitOfWork))) { + return false; + } + } + + return true; +} + } +} + diff --git a/src/Fluss.UnitTest/Setup.cs b/src/Fluss.UnitTest/Setup.cs new file mode 100644 index 0000000..f72ce85 --- /dev/null +++ b/src/Fluss.UnitTest/Setup.cs @@ -0,0 +1,13 @@ +using System.Runtime.CompilerServices; + +namespace Fluss.UnitTest; + +public static class Setup +{ + [ModuleInitializer] + public static void Init() + { + VerifySourceGenerators.Initialize(); + Verifier.UseSourceFileRelativeDirectory("Snapshots"); + } +} \ No newline at end of file diff --git a/src/Fluss.sln b/src/Fluss.sln index d9339a0..1154d39 100644 --- a/src/Fluss.sln +++ b/src/Fluss.sln @@ -10,11 +10,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Fluss.UnitTest", "Fluss.Uni EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Fluss.Testing", "Fluss.Testing\Fluss.Testing.csproj", "{EB267687-7C88-4DF4-85E4-ACE3FC41BB73}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Fluss.Regen", "Fluss.Regen\Fluss.Regen\Fluss.Regen.csproj", "{6E250321-6993-4B94-8600-85E15BF713FA}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Fluss.Regen.Tests", "Fluss.Regen\Fluss.Regen.Tests\Fluss.Regen.Tests.csproj", "{A4D8B2DD-0046-48FE-A765-A6E032A13FAE}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Fluss.Sample", "Fluss.Sample\Fluss.Sample.csproj", "{A5D49E3A-163D-4BF4-830A-C5384B21859F}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Fluss.Regen", "Fluss.Regen\Fluss.Regen.csproj", "{6E250321-6993-4B94-8600-85E15BF713FA}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -46,13 +42,5 @@ Global {6E250321-6993-4B94-8600-85E15BF713FA}.Debug|Any CPU.Build.0 = Debug|Any CPU {6E250321-6993-4B94-8600-85E15BF713FA}.Release|Any CPU.ActiveCfg = Release|Any CPU {6E250321-6993-4B94-8600-85E15BF713FA}.Release|Any CPU.Build.0 = Release|Any CPU - {A4D8B2DD-0046-48FE-A765-A6E032A13FAE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A4D8B2DD-0046-48FE-A765-A6E032A13FAE}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A4D8B2DD-0046-48FE-A765-A6E032A13FAE}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A4D8B2DD-0046-48FE-A765-A6E032A13FAE}.Release|Any CPU.Build.0 = Release|Any CPU - {A5D49E3A-163D-4BF4-830A-C5384B21859F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A5D49E3A-163D-4BF4-830A-C5384B21859F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A5D49E3A-163D-4BF4-830A-C5384B21859F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A5D49E3A-163D-4BF4-830A-C5384B21859F}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/src/Fluss.sln.DotSettings.user b/src/Fluss.sln.DotSettings.user index a7aeee0..7c371f3 100644 --- a/src/Fluss.sln.DotSettings.user +++ b/src/Fluss.sln.DotSettings.user @@ -1,7 +1,10 @@  + C:\Users\Enterprize1\AppData\Local\JetBrains\Rider2024.2\resharper-host\temp\Rider\vAny\CoverageData\_Fluss.-842573491\Snapshot\snapshot.utdcvr + <SessionState ContinuousTestingMode="0" IsActive="True" Name="Session" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"> - <Project Location="C:\Users\Enterprize1\Code\fluss\src\Fluss.Regen\Fluss.Regen.Tests" Presentation="&lt;Fluss.Regen.Tests&gt;" /> + <Project Location="C:\Users\Enterprize1\Code\fluss\src\Fluss.UnitTest" Presentation="&lt;Fluss.UnitTest&gt;" /> </SessionState> + DoNothing diff --git a/src/Fluss/Events/EventListener.cs b/src/Fluss/Events/EventListener.cs index e10bfc0..ee47052 100644 --- a/src/Fluss/Events/EventListener.cs +++ b/src/Fluss/Events/EventListener.cs @@ -74,7 +74,18 @@ public interface IRootEventListener { } -public interface IEventListenerWithKey +public interface IEventListenerWithKey { - public TKey Id { get; init; } + public object Id { get; init; } +} + +public interface IEventListenerWithKey : IEventListenerWithKey +{ + public new TKey Id { get; init; } + + object IEventListenerWithKey.Id + { + get => Id!; + init => Id = (TKey)value; + } } \ No newline at end of file diff --git a/src/Fluss/UnitOfWork/IUnitOfWork.cs b/src/Fluss/UnitOfWork/IUnitOfWork.cs index e938933..abfb966 100644 --- a/src/Fluss/UnitOfWork/IUnitOfWork.cs +++ b/src/Fluss/UnitOfWork/IUnitOfWork.cs @@ -1,20 +1,17 @@ using System.Collections.Concurrent; -using Fluss.Aggregates; using Fluss.Events; using Fluss.ReadModel; namespace Fluss; -public partial interface IUnitOfWork +public interface IUnitOfWork { - ValueTask GetAggregate(TKey key) - where TAggregate : AggregateRoot, new(); - - ValueTask Publish(Event @event, AggregateRoot? aggregate = null); ValueTask ConsistentVersion(); IReadOnlyCollection ReadModels { get; } ConcurrentQueue PublishedEventEnvelopes { get; } + ValueTask GetReadModel(Type tReadModel, object? key, long? at = null); + ValueTask GetReadModel(long? at = null) where TReadModel : EventListener, IRootEventListener, IReadModel, new(); diff --git a/src/Fluss/UnitOfWork/IWriteUnitOfWork.cs b/src/Fluss/UnitOfWork/IWriteUnitOfWork.cs new file mode 100644 index 0000000..6977f4d --- /dev/null +++ b/src/Fluss/UnitOfWork/IWriteUnitOfWork.cs @@ -0,0 +1,14 @@ +using System.Collections.Concurrent; +using Fluss.Aggregates; +using Fluss.Events; +using Fluss.ReadModel; + +namespace Fluss; + +public interface IWriteUnitOfWork : IUnitOfWork +{ + ValueTask GetAggregate(TKey key) + where TAggregate : AggregateRoot, new(); + + ValueTask Publish(Event @event, AggregateRoot? aggregate = null); +} diff --git a/src/Fluss/UnitOfWork/UnitOfWork.ReadModels.cs b/src/Fluss/UnitOfWork/UnitOfWork.ReadModels.cs index d840542..f2261e4 100644 --- a/src/Fluss/UnitOfWork/UnitOfWork.ReadModels.cs +++ b/src/Fluss/UnitOfWork/UnitOfWork.ReadModels.cs @@ -9,6 +9,41 @@ public partial class UnitOfWork private readonly ConcurrentBag _readModels = new(); public IReadOnlyCollection ReadModels => _readModels; + public async ValueTask GetReadModel(Type tReadModel, object? key, long? at = null) + { + using var activity = FlussActivitySource.Source.StartActivity(); + activity?.SetTag("EventSourcing.ReadModel", tReadModel.FullName); + + if (Activator.CreateInstance(tReadModel) is not EventListener eventListener) + { + throw new InvalidOperationException("Type " + tReadModel.FullName + " is not a event listener."); + } + + if (eventListener is IEventListenerWithKey eventListenerWithKey) + { + eventListenerWithKey.GetType().GetProperty("Id")?.SetValue(eventListenerWithKey, key); + } + + eventListener = await UpdateAndApplyPublished(eventListener, at); + + if (eventListener is not IReadModel readModel) + { + throw new InvalidOperationException("Type " + tReadModel.FullName + " is not a read model."); + } + + if (!await AuthorizeUsage(readModel)) + { + throw new UnauthorizedAccessException($"Cannot read {eventListener.GetType()} as the current user."); + } + + if (at is null) + { + _readModels.Add(eventListener); + } + + return readModel; + } + public async ValueTask GetReadModel(long? at = null) where TReadModel : EventListener, IRootEventListener, IReadModel, new() { diff --git a/src/Fluss/UnitOfWork/UnitOfWork.cs b/src/Fluss/UnitOfWork/UnitOfWork.cs index 7812f6c..8b76dc7 100644 --- a/src/Fluss/UnitOfWork/UnitOfWork.cs +++ b/src/Fluss/UnitOfWork/UnitOfWork.cs @@ -4,7 +4,7 @@ namespace Fluss; -public partial class UnitOfWork : IUnitOfWork +public partial class UnitOfWork : IWriteUnitOfWork { private readonly IEventListenerFactory _eventListenerFactory; private readonly IEventRepository _eventRepository; diff --git a/src/Fluss/UnitOfWork/UnitOfWorkFactory.cs b/src/Fluss/UnitOfWork/UnitOfWorkFactory.cs index e41351e..bcbca28 100644 --- a/src/Fluss/UnitOfWork/UnitOfWorkFactory.cs +++ b/src/Fluss/UnitOfWork/UnitOfWorkFactory.cs @@ -23,7 +23,7 @@ public UnitOfWorkFactory(IServiceProvider serviceProvider) .Handle() .WaitAndRetryAsync(Delay); - public async ValueTask Commit(Func action) + public async ValueTask Commit(Func action) { using var activity = FlussActivitySource.Source.StartActivity(); @@ -36,7 +36,7 @@ await RetryPolicy }); } - public async ValueTask Commit(Func> action) + public async ValueTask Commit(Func> action) { using var activity = FlussActivitySource.Source.StartActivity(); diff --git a/src/Fluss/UnitOfWork/UnitOfWorkRecordingProxy.cs b/src/Fluss/UnitOfWork/UnitOfWorkRecordingProxy.cs new file mode 100644 index 0000000..12678f1 --- /dev/null +++ b/src/Fluss/UnitOfWork/UnitOfWorkRecordingProxy.cs @@ -0,0 +1,110 @@ +using System.Collections.Concurrent; +using Fluss.Events; +using Fluss.ReadModel; + +namespace Fluss; + +public class UnitOfWorkRecordingProxy : IUnitOfWork +{ + private readonly IUnitOfWork _impl; + + public UnitOfWorkRecordingProxy(IUnitOfWork impl) + { + _impl = impl; + } + + public ValueTask ConsistentVersion() + { + return _impl.ConsistentVersion(); + } + + public IReadOnlyCollection ReadModels => _impl.ReadModels; + public ConcurrentQueue PublishedEventEnvelopes => _impl.PublishedEventEnvelopes; + + public List RecordedListeners { get; } = new List(); + + public ValueTask GetReadModel(Type tReadModel, object key, long? at = null) + { + return _impl.GetReadModel(tReadModel, key, at); + } + + public ValueTask GetReadModel(long? at = null) where TReadModel : EventListener, IRootEventListener, IReadModel, new() + { + return Record(_impl.GetReadModel(at)); + } + + public ValueTask GetReadModel(TKey key, long? at = null) where TReadModel : EventListener, IEventListenerWithKey, IReadModel, new() + { + return Record(_impl.GetReadModel(key, at)); + } + + public ValueTask UnsafeGetReadModelWithoutAuthorization(long? at = null) where TReadModel : EventListener, IRootEventListener, IReadModel, new() + { + return Record(_impl.UnsafeGetReadModelWithoutAuthorization(at)); + } + + public ValueTask UnsafeGetReadModelWithoutAuthorization(TKey key, long? at = null) where TReadModel : EventListener, IEventListenerWithKey, IReadModel, new() + { + return Record(_impl.UnsafeGetReadModelWithoutAuthorization(key, at)); + } + + public ValueTask> GetMultipleReadModels(IEnumerable keys, long? at = null) where TReadModel : EventListener, IReadModel, IEventListenerWithKey, new() where TKey : notnull + { + return Record(_impl.GetMultipleReadModels(keys, at)); + } + + public ValueTask> UnsafeGetMultipleReadModelsWithoutAuthorization(IEnumerable keys, long? at = null) where TReadModel : EventListener, IReadModel, IEventListenerWithKey, new() where TKey : notnull + { + return Record(_impl.UnsafeGetMultipleReadModelsWithoutAuthorization(keys, at)); + } + + public IUnitOfWork WithPrefilledVersion(long? version) + { + return _impl.WithPrefilledVersion(version); + } + + private async ValueTask Record(ValueTask readModel) where TReadModel : EventListener + { + var result = await readModel; + RecordedListeners.Add(result); + return result; + } + + private async ValueTask> Record(ValueTask> readModel) where TReadModel : EventListener + { + var result = await readModel; + RecordedListeners.AddRange(result); + return result; + } + + public IReadOnlyList GetRecordedListeners() + { + var eventListenerTypeWithKeyAndVersions = new List(); + + foreach (var recordedListener in RecordedListeners) + { + eventListenerTypeWithKeyAndVersions.Add(new EventListenerTypeWithKeyAndVersion( + recordedListener.GetType(), + recordedListener is IEventListenerWithKey keyListener ? keyListener.Id : null, + recordedListener.Tag.LastAccepted + )); + } + + return eventListenerTypeWithKeyAndVersions; + } + + public record EventListenerTypeWithKeyAndVersion(Type Type, object? Key, long Version) + { + public async ValueTask IsStillUpToDate(IUnitOfWork unitOfWork, long? at = null) + { + var readModel = await unitOfWork.GetReadModel(Type, Key, at); + + if (readModel is EventListener eventListener) + { + return eventListener.Tag.LastAccepted <= Version; + } + + return false; + } + } +} \ No newline at end of file diff --git a/src/Fluss/Validation/RootValidator.cs b/src/Fluss/Validation/RootValidator.cs index b27dc03..24572ca 100644 --- a/src/Fluss/Validation/RootValidator.cs +++ b/src/Fluss/Validation/RootValidator.cs @@ -111,4 +111,3 @@ public async Task ValidateAggregate(AggregateRoot aggregate, UnitOfWork unitOfWo } } } - \ No newline at end of file