diff --git a/src/StrawberryShake/Client/StrawberryShake.Client.slnx b/src/StrawberryShake/Client/StrawberryShake.Client.slnx index d012b3a0532..aa977b1449a 100644 --- a/src/StrawberryShake/Client/StrawberryShake.Client.slnx +++ b/src/StrawberryShake/Client/StrawberryShake.Client.slnx @@ -11,6 +11,7 @@ + diff --git a/src/StrawberryShake/Client/src/Core/IOperationResultBuilder.cs b/src/StrawberryShake/Client/src/Core/IOperationResultBuilder.cs index ce757b906a9..fcf73a23d4b 100644 --- a/src/StrawberryShake/Client/src/Core/IOperationResultBuilder.cs +++ b/src/StrawberryShake/Client/src/Core/IOperationResultBuilder.cs @@ -25,4 +25,19 @@ public interface IOperationResultBuilder /// IOperationResult Build( Response response); + + /// + /// Builds a runtime operation result from a previously persisted transport "data" + /// payload, without executing the operation. This is used to rehydrate state that was + /// captured during a server prerender. + /// + /// + /// The UTF-8 encoded JSON of the GraphQL response "data" object. + /// + /// + /// Returns the runtime result. + /// + IOperationResult BuildFromPersistedData(ReadOnlyMemory persistedData) + => throw new NotSupportedException( + "This operation result builder does not support rehydrating from persisted data."); } diff --git a/src/StrawberryShake/Client/src/Core/OperationResultBuilder.cs b/src/StrawberryShake/Client/src/Core/OperationResultBuilder.cs index 0018c1ea549..99b6f140ce8 100644 --- a/src/StrawberryShake/Client/src/Core/OperationResultBuilder.cs +++ b/src/StrawberryShake/Client/src/Core/OperationResultBuilder.cs @@ -1,3 +1,4 @@ +using System.Buffers; using System.Text.Json; using StrawberryShake.Json; using static StrawberryShake.ResultFields; @@ -24,6 +25,7 @@ public IOperationResult Build( IOperationResultDataInfo? dataInfo = null; IReadOnlyList? errors = null; IReadOnlyDictionary? extensions = null; + JsonElement? persistedData = null; try { @@ -34,6 +36,11 @@ public IOperationResult Build( { dataInfo = BuildData(dataProp); data = ResultDataFactory.Create(dataInfo); + + if (CapturePersistedData) + { + persistedData = dataProp.Clone(); + } } if (body.RootElement.TryGetProperty(Errors, out var errorsProp) @@ -90,13 +97,58 @@ public IOperationResult Build( }; } + var contextData = response.ContextData; + + if (persistedData is { } captured) + { + var augmented = contextData is null + ? new Dictionary() + : new Dictionary(contextData); + augmented[WellKnownContextData.PersistedData] = captured; + contextData = augmented; + } + return new OperationResult( data, dataInfo, ResultDataFactory, errors, extensions, - response.ContextData); + contextData); + } + + /// + /// When overridden to return true, the raw transport "data" payload is captured + /// into under + /// so it can be persisted and later + /// rehydrated via . + /// + protected virtual bool CapturePersistedData => false; + + /// + /// Builds a runtime operation result from a previously persisted transport "data" + /// payload, without executing the operation. + /// + /// + /// The UTF-8 encoded JSON of the GraphQL response "data" object. + /// + /// + /// Returns the runtime result. + /// + public IOperationResult BuildFromPersistedData(ReadOnlyMemory persistedData) + { + var bufferWriter = new ArrayBufferWriter(); + + using (var writer = new Utf8JsonWriter(bufferWriter)) + { + writer.WriteStartObject(); + writer.WritePropertyName(Data); + writer.WriteRawValue(persistedData.Span, skipInputValidation: true); + writer.WriteEndObject(); + } + + using var document = JsonDocument.Parse(bufferWriter.WrittenMemory); + return Build(new Response(document, null)); } protected abstract IOperationResultDataInfo BuildData(JsonElement obj); diff --git a/src/StrawberryShake/Client/src/Core/WellKnownContextData.cs b/src/StrawberryShake/Client/src/Core/WellKnownContextData.cs new file mode 100644 index 00000000000..e27b8012c62 --- /dev/null +++ b/src/StrawberryShake/Client/src/Core/WellKnownContextData.cs @@ -0,0 +1,15 @@ +namespace StrawberryShake; + +/// +/// Well known keys for . +/// +public static class WellKnownContextData +{ + /// + /// The key under which the raw transport "data" payload of an operation result is + /// captured. This allows the payload to be persisted (for example via Blazor's + /// PersistentComponentState) and later rehydrated without re-executing the + /// operation. + /// + public const string PersistedData = "StrawberryShake.PersistedData"; +} diff --git a/src/StrawberryShake/Client/src/Razor/OperationResultPersistentStateSerializer.cs b/src/StrawberryShake/Client/src/Razor/OperationResultPersistentStateSerializer.cs new file mode 100644 index 00000000000..f12e67c9166 --- /dev/null +++ b/src/StrawberryShake/Client/src/Razor/OperationResultPersistentStateSerializer.cs @@ -0,0 +1,58 @@ +#if NET10_0_OR_GREATER +using System.Buffers; +using System.Text.Json; +using Microsoft.AspNetCore.Components; + +namespace StrawberryShake.Razor; + +/// +/// A for StrawberryShake operation +/// results. It persists the raw transport "data" payload that was captured on the result and +/// rehydrates it through the operation's result builder. This lets a property of type +/// IOperationResult<TData> annotated with [PersistentState] survive the +/// prerender to interactive boundary without re-executing the operation. +/// +/// +/// The operation result type. +/// +public sealed class OperationResultPersistentStateSerializer + : PersistentComponentStateSerializer> + where TData : class +{ + private static readonly byte[] s_emptyData = "{}"u8.ToArray(); + + private readonly IOperationResultBuilder _resultBuilder; + + public OperationResultPersistentStateSerializer( + IOperationResultBuilder resultBuilder) + { + ArgumentNullException.ThrowIfNull(resultBuilder); + + _resultBuilder = resultBuilder; + } + + public override void Persist(IOperationResult value, IBufferWriter writer) + { + ArgumentNullException.ThrowIfNull(value); + ArgumentNullException.ThrowIfNull(writer); + + if (value.ContextData.TryGetValue(WellKnownContextData.PersistedData, out var payload) + && payload is JsonElement element) + { + using var jsonWriter = new Utf8JsonWriter(writer); + element.WriteTo(jsonWriter); + } + else + { + // No captured payload (for example an error result): persist an empty data + // object so the value still round-trips to a non-null result. + writer.Write(s_emptyData); + } + } + + public override IOperationResult Restore(ReadOnlySequence data) + { + return _resultBuilder.BuildFromPersistedData(data.ToArray()); + } +} +#endif diff --git a/src/StrawberryShake/Client/src/Razor/StrawberryShakePersistentStateServiceCollectionExtensions.cs b/src/StrawberryShake/Client/src/Razor/StrawberryShakePersistentStateServiceCollectionExtensions.cs new file mode 100644 index 00000000000..c8079c1d65b --- /dev/null +++ b/src/StrawberryShake/Client/src/Razor/StrawberryShakePersistentStateServiceCollectionExtensions.cs @@ -0,0 +1,43 @@ +#if NET10_0_OR_GREATER +using Microsoft.AspNetCore.Components; +using Microsoft.Extensions.DependencyInjection; + +namespace StrawberryShake.Razor; + +/// +/// Service collection extensions for persisting StrawberryShake operation results across +/// the Blazor prerender to interactive boundary. +/// +public static class StrawberryShakePersistentStateServiceCollectionExtensions +{ + /// + /// Registers a for + /// IOperationResult<TData>, so that a component or service property of that + /// type annotated with [PersistentState] is persisted during a server prerender + /// and rehydrated on the interactive client without re-executing the operation. + /// + /// + /// The operation result type (the type argument of IOperationResult<>). + /// + /// + /// The service collection. The matching StrawberryShake client must already be + /// registered so that IOperationResultBuilder<JsonDocument, TData> can be + /// resolved. + /// + /// + /// The service collection so that further calls can be chained. + /// + public static IServiceCollection AddPersistentOperationResult( + this IServiceCollection services) + where TData : class + { + ArgumentNullException.ThrowIfNull(services); + + services.AddSingleton< + PersistentComponentStateSerializer>, + OperationResultPersistentStateSerializer>(); + + return services; + } +} +#endif diff --git a/src/StrawberryShake/Client/test/Core.Tests/OperationResultBuilderTests.cs b/src/StrawberryShake/Client/test/Core.Tests/OperationResultBuilderTests.cs index 59f0816266b..36b6631f6a5 100644 --- a/src/StrawberryShake/Client/test/Core.Tests/OperationResultBuilderTests.cs +++ b/src/StrawberryShake/Client/test/Core.Tests/OperationResultBuilderTests.cs @@ -1,3 +1,4 @@ +using System.Buffers; using System.Text.Json; namespace StrawberryShake; @@ -35,6 +36,72 @@ public void Build_With_Extensions() Assert.Equal(3.14, result.Extensions["d"]); } + [Fact] + public void Build_With_Capture_Stores_Data_Payload_In_ContextData() + { + // arrange + var builder = new RecordingOperationResultBuilder(new DocumentDataFactory()); + var response = new Response( + JsonDocument.Parse(@"{""data"": { ""name"": ""Strawberry"" } }"), + null); + + // act + var result = builder.Build(response); + + // assert + Assert.True( + result.ContextData.TryGetValue(WellKnownContextData.PersistedData, out var payload)); + var element = Assert.IsType(payload); + Assert.Equal("Strawberry", element.GetProperty("name").GetString()); + } + + [Fact] + public void Build_Without_Capture_Does_Not_Store_Data_Payload() + { + // arrange + var builder = new DocumentOperationResultBuilder(new DocumentDataFactory()); + var response = new Response( + JsonDocument.Parse(@"{""data"": { ""name"": ""Strawberry"" } }"), + null); + + // act + var result = builder.Build(response); + + // assert + Assert.Empty(result.ContextData); + } + + [Fact] + public void BuildFromPersistedData_RoundTrips_The_Captured_Payload() + { + // arrange + var builder = new RecordingOperationResultBuilder(new DocumentDataFactory()); + var captured = builder.Build( + new Response( + JsonDocument.Parse(@"{""data"": { ""name"": ""Strawberry"" } }"), + null)); + var payload = (JsonElement)captured.ContextData[WellKnownContextData.PersistedData]!; + + // act + var rehydrated = builder.BuildFromPersistedData(ToUtf8(payload)); + + // assert + Assert.NotNull(rehydrated.Data); + Assert.Equal("Strawberry", builder.LastData!.Value.GetProperty("name").GetString()); + } + + private static ReadOnlyMemory ToUtf8(JsonElement element) + { + var buffer = new ArrayBufferWriter(); + + using (var writer = new Utf8JsonWriter(buffer)) + { + element.WriteTo(writer); + } + + return buffer.WrittenMemory; + } + internal class Document; internal class DocumentDataInfo : IOperationResultDataInfo @@ -78,4 +145,25 @@ protected override IOperationResultDataInfo BuildData(JsonElement obj) return new DocumentDataInfo(); } } + + internal sealed class RecordingOperationResultBuilder : OperationResultBuilder + { + public RecordingOperationResultBuilder( + IOperationResultDataFactory resultDataFactory) + { + ResultDataFactory = resultDataFactory; + } + + public JsonElement? LastData { get; private set; } + + protected override bool CapturePersistedData => true; + + protected override IOperationResultDataFactory ResultDataFactory { get; } + + protected override IOperationResultDataInfo BuildData(JsonElement obj) + { + LastData = obj.Clone(); + return new DocumentDataInfo(); + } + } } diff --git a/src/StrawberryShake/Client/test/Razor.Tests/OperationResultPersistentStateSerializerTests.cs b/src/StrawberryShake/Client/test/Razor.Tests/OperationResultPersistentStateSerializerTests.cs new file mode 100644 index 00000000000..dc677434d03 --- /dev/null +++ b/src/StrawberryShake/Client/test/Razor.Tests/OperationResultPersistentStateSerializerTests.cs @@ -0,0 +1,109 @@ +using System.Buffers; +using System.Text; +using System.Text.Json; + +namespace StrawberryShake.Razor; + +public class OperationResultPersistentStateSerializerTests +{ + [Fact] + public void RoundTrips_Result_Without_Rebuilding_From_The_Network() + { + // arrange + var builder = new CapturingResultBuilder(new TestDataFactory()); + var captured = builder.Build( + new Response( + JsonDocument.Parse(@"{""data"": { ""name"": ""Strawberry"" } }"), + null)); + var serializer = new OperationResultPersistentStateSerializer(builder); + var buffer = new ArrayBufferWriter(); + + // act + serializer.Persist(captured, buffer); + var restored = serializer.Restore(new ReadOnlySequence(buffer.WrittenMemory)); + + // assert + Assert.NotNull(restored.Data); + Assert.Equal("Strawberry", builder.LastData!.Value.GetProperty("name").GetString()); + } + + [Fact] + public void Persist_Without_Captured_Payload_Falls_Back_To_Empty_Data_Object() + { + // arrange + // A result built without capture carries no persisted payload (e.g. an error result). + var plain = new PlainResultBuilder(new TestDataFactory()).Build( + new Response( + JsonDocument.Parse(@"{""data"": { ""name"": ""Strawberry"" } }"), + null)); + var serializer = new OperationResultPersistentStateSerializer( + new CapturingResultBuilder(new TestDataFactory())); + var buffer = new ArrayBufferWriter(); + + // act + serializer.Persist(plain, buffer); + var restored = serializer.Restore(new ReadOnlySequence(buffer.WrittenMemory)); + + // assert + Assert.Equal("{}", Encoding.UTF8.GetString(buffer.WrittenSpan)); + Assert.NotNull(restored.Data); + } + + private sealed class TestData; + + private sealed class TestDataInfo : IOperationResultDataInfo + { + public IReadOnlyCollection EntityIds { get; } = ArraySegment.Empty; + + public ulong Version { get; } + + public IOperationResultDataInfo WithVersion(ulong version) + => throw new NotImplementedException(); + } + + private sealed class TestDataFactory : IOperationResultDataFactory + { + public Type ResultType => typeof(TestData); + + public TestData Create(IOperationResultDataInfo dataInfo, IEntityStoreSnapshot? snapshot = null) + => new(); + + object IOperationResultDataFactory.Create( + IOperationResultDataInfo dataInfo, + IEntityStoreSnapshot? snapshot) + => Create(dataInfo, snapshot); + } + + private sealed class CapturingResultBuilder : OperationResultBuilder + { + public CapturingResultBuilder(IOperationResultDataFactory resultDataFactory) + { + ResultDataFactory = resultDataFactory; + } + + public JsonElement? LastData { get; private set; } + + protected override bool CapturePersistedData => true; + + protected override IOperationResultDataFactory ResultDataFactory { get; } + + protected override IOperationResultDataInfo BuildData(JsonElement obj) + { + LastData = obj.Clone(); + return new TestDataInfo(); + } + } + + private sealed class PlainResultBuilder : OperationResultBuilder + { + public PlainResultBuilder(IOperationResultDataFactory resultDataFactory) + { + ResultDataFactory = resultDataFactory; + } + + protected override IOperationResultDataFactory ResultDataFactory { get; } + + protected override IOperationResultDataInfo BuildData(JsonElement obj) + => new TestDataInfo(); + } +} diff --git a/src/StrawberryShake/Client/test/Razor.Tests/StrawberryShake.Razor.Tests.csproj b/src/StrawberryShake/Client/test/Razor.Tests/StrawberryShake.Razor.Tests.csproj new file mode 100644 index 00000000000..cb439f6db37 --- /dev/null +++ b/src/StrawberryShake/Client/test/Razor.Tests/StrawberryShake.Razor.Tests.csproj @@ -0,0 +1,16 @@ + + + + + + StrawberryShake.Razor.Tests + StrawberryShake.Razor + + net11.0;net10.0 + + + + + + + diff --git a/src/StrawberryShake/CodeGeneration/src/CodeGeneration.CSharp/CSharpGenerator.cs b/src/StrawberryShake/CodeGeneration/src/CodeGeneration.CSharp/CSharpGenerator.cs index 9d1b9f01c85..57e3df4aded 100644 --- a/src/StrawberryShake/CodeGeneration/src/CodeGeneration.CSharp/CSharpGenerator.cs +++ b/src/StrawberryShake/CodeGeneration/src/CodeGeneration.CSharp/CSharpGenerator.cs @@ -306,7 +306,8 @@ private static IReadOnlyList GenerateCSharpDocuments( settings.NoStore, settings.InputRecords, settings.EntityRecords, - settings.RazorComponents); + settings.RazorComponents, + settings.RazorPersistedState); var results = new List(); diff --git a/src/StrawberryShake/CodeGeneration/src/CodeGeneration.CSharp/CSharpGeneratorSettings.cs b/src/StrawberryShake/CodeGeneration/src/CodeGeneration.CSharp/CSharpGeneratorSettings.cs index 438ea7aafbb..13d1d4a5709 100644 --- a/src/StrawberryShake/CodeGeneration/src/CodeGeneration.CSharp/CSharpGeneratorSettings.cs +++ b/src/StrawberryShake/CodeGeneration/src/CodeGeneration.CSharp/CSharpGeneratorSettings.cs @@ -48,6 +48,12 @@ public class CSharpGeneratorSettings /// public bool RazorComponents { get; set; } + /// + /// Emit persisted component state wiring so operation results can be persisted via the + /// .NET 10 [PersistentState] attribute (requires a store). + /// + public bool RazorPersistedState { get; set; } + /// /// Generate a single CSharp code file. /// diff --git a/src/StrawberryShake/CodeGeneration/src/CodeGeneration.CSharp/Generators/DependencyInjectionGenerator.cs b/src/StrawberryShake/CodeGeneration/src/CodeGeneration.CSharp/Generators/DependencyInjectionGenerator.cs index d6e8696f930..f9c7061efb6 100644 --- a/src/StrawberryShake/CodeGeneration/src/CodeGeneration.CSharp/Generators/DependencyInjectionGenerator.cs +++ b/src/StrawberryShake/CodeGeneration/src/CodeGeneration.CSharp/Generators/DependencyInjectionGenerator.cs @@ -627,7 +627,7 @@ private static ICode RegisterOperation( string factory, string resultBuilder) { - return CodeBlockBuilder + var code = CodeBlockBuilder .New() .AddCode( MethodCallBuilder @@ -756,6 +756,28 @@ private static ICode RegisterOperation( .SetMethodName(GetRequiredService) .AddGeneric(operationFullName) .AddArgument(Sp)))); + + // Register a persistent component state serializer so the operation result can be + // persisted via the .NET 10 [PersistentState] attribute and rehydrated without + // re-executing the operation. Guarded so consumers targeting older frameworks (where + // the serializer type does not exist) still compile. + if (settings.RazorPersistedState && settings.IsStoreEnabled()) + { + code + .AddCode(CodeLineBuilder.From("#if NET10_0_OR_GREATER")) + .AddCode(MethodCallBuilder + .New() + .SetMethodName(AddSingleton) + .AddGeneric( + PersistentComponentStateSerializer + .WithGeneric(IOperationResult.WithGeneric(resultInterface))) + .AddGeneric( + OperationResultPersistentStateSerializer.WithGeneric(resultInterface)) + .AddArgument(Services)) + .AddCode(CodeLineBuilder.From("#endif")); + } + + return code; } private static ICode RegisterHttpConnection(string clientName) => diff --git a/src/StrawberryShake/CodeGeneration/src/CodeGeneration.CSharp/Generators/JsonResultBuilderGenerator.cs b/src/StrawberryShake/CodeGeneration/src/CodeGeneration.CSharp/Generators/JsonResultBuilderGenerator.cs index c10f15b0359..bb9f219aa81 100644 --- a/src/StrawberryShake/CodeGeneration/src/CodeGeneration.CSharp/Generators/JsonResultBuilderGenerator.cs +++ b/src/StrawberryShake/CodeGeneration/src/CodeGeneration.CSharp/Generators/JsonResultBuilderGenerator.cs @@ -72,6 +72,18 @@ resultBuilderDescriptor.ResultNamedType as InterfaceTypeDescriptor ?? .SetAccessModifier(AccessModifier.Protected) .SetOverride()); + // Capture the raw transport "data" payload so the result can be persisted via the + // .NET 10 [PersistentState] attribute and rehydrated without re-executing. + if (settings.RazorPersistedState && settings.IsStoreEnabled()) + { + classBuilder + .AddProperty("CapturePersistedData") + .SetType("global::System.Boolean") + .SetAccessModifier(AccessModifier.Protected) + .SetOverride() + .AsLambda("true"); + } + var assignment = AssignmentBuilder .New() .SetLeftHandSide(GetPropertyName(ResultDataFactory)) diff --git a/src/StrawberryShake/CodeGeneration/src/CodeGeneration/CSharpSyntaxGeneratorSettings.cs b/src/StrawberryShake/CodeGeneration/src/CodeGeneration/CSharpSyntaxGeneratorSettings.cs index 45039d1f034..e1b3a3713f7 100644 --- a/src/StrawberryShake/CodeGeneration/src/CodeGeneration/CSharpSyntaxGeneratorSettings.cs +++ b/src/StrawberryShake/CodeGeneration/src/CodeGeneration/CSharpSyntaxGeneratorSettings.cs @@ -13,13 +13,15 @@ public CSharpSyntaxGeneratorSettings( bool noStore, bool inputRecords, bool entityRecords, - bool razorComponents) + bool razorComponents, + bool razorPersistedState) { AccessModifier = accessModifier; NoStore = noStore; InputRecords = inputRecords; EntityRecords = entityRecords; RazorComponents = razorComponents; + RazorPersistedState = razorPersistedState; } /// @@ -46,4 +48,11 @@ public CSharpSyntaxGeneratorSettings( /// Generate Razor components. /// public bool RazorComponents { get; } + + /// + /// Emit persisted component state wiring (raw payload capture and a + /// PersistentComponentStateSerializer registration per result type) so operation + /// results can be persisted via the .NET 10 [PersistentState] attribute. + /// + public bool RazorPersistedState { get; } } diff --git a/src/StrawberryShake/CodeGeneration/src/CodeGeneration/TypeNames.cs b/src/StrawberryShake/CodeGeneration/src/CodeGeneration/TypeNames.cs index 4b24031ce79..5a0096e407a 100644 --- a/src/StrawberryShake/CodeGeneration/src/CodeGeneration/TypeNames.cs +++ b/src/StrawberryShake/CodeGeneration/src/CodeGeneration/TypeNames.cs @@ -243,6 +243,12 @@ public static class TypeNames public const string UseSubscription = StrawberryShakeNamespace + "Razor." + nameof(UseSubscription); + public const string PersistentComponentStateSerializer = + "global::Microsoft.AspNetCore.Components." + nameof(PersistentComponentStateSerializer); + + public const string OperationResultPersistentStateSerializer = + StrawberryShakeNamespace + "Razor." + nameof(OperationResultPersistentStateSerializer); + public const string Upload = StrawberryShakeNamespace + nameof(Upload); public const string StringSerializer = diff --git a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.Razor.Tests/GeneratorTestHelper.cs b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.Razor.Tests/GeneratorTestHelper.cs index 681b6e5b10b..c20d0bdcd3d 100644 --- a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.Razor.Tests/GeneratorTestHelper.cs +++ b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.Razor.Tests/GeneratorTestHelper.cs @@ -89,7 +89,8 @@ public static void AssertResult( NoStore = settings.NoStore, InputRecords = settings.InputRecords, EntityRecords = settings.EntityRecords, - RazorComponents = settings.RazorComponents + RazorComponents = settings.RazorComponents, + RazorPersistedState = settings.RazorPersistedState }); Assert.False( @@ -308,6 +309,8 @@ public class AssertSettings public bool RazorComponents { get; set; } + public bool RazorPersistedState { get; set; } + public List Profiles { get; set; } = []; public RequestStrategyGen RequestStrategy { get; set; } = diff --git a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.Razor.Tests/RazorGeneratorTests.cs b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.Razor.Tests/RazorGeneratorTests.cs index 506d2382e77..0eeeea9b9fa 100644 --- a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.Razor.Tests/RazorGeneratorTests.cs +++ b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.Razor.Tests/RazorGeneratorTests.cs @@ -44,4 +44,33 @@ type Bar { """, "extend schema @key(fields: \"id\")"); } + + [Fact] + public void Query_With_Persisted_State() + { + // force assembly to load! + Assert.NotNull(typeof(UseQuery<>)); + + AssertResult( + settings: new() { RazorPersistedState = true }, + """ + query GetBars($a: String! $b: String) { + bars(a: $a b: $b) { + id + name + } + } + """, + """ + type Query { + bars(a: String!, b: String): [Bar] + } + + type Bar { + id: String! + name: String + } + """, + "extend schema @key(fields: \"id\")"); + } } diff --git a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.Razor.Tests/__snapshots__/RazorGeneratorTests.Query_With_Persisted_State.snap b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.Razor.Tests/__snapshots__/RazorGeneratorTests.Query_With_Persisted_State.snap new file mode 100644 index 00000000000..c3cd6b78c84 --- /dev/null +++ b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.Razor.Tests/__snapshots__/RazorGeneratorTests.Query_With_Persisted_State.snap @@ -0,0 +1,778 @@ +// ReSharper disable ArrangeObjectCreationWhenTypeEvident +// ReSharper disable BuiltInTypeReferenceStyle +// ReSharper disable ConvertToAutoProperty +// ReSharper disable InconsistentNaming +// ReSharper disable PartialTypeWithSinglePart +// ReSharper disable PreferConcreteValueOverDefault +// ReSharper disable RedundantNameQualifier +// ReSharper disable SuggestVarOrType_SimpleTypes +// ReSharper disable UnusedMember.Global +// ReSharper disable UnusedMethodReturnValue.Local +// ReSharper disable UnusedType.Global +// ReSharper disable UnusedVariable + +// FooClient + +// +#nullable enable annotations +#nullable disable warnings + +namespace Foo.Bar +{ + // StrawberryShake.CodeGeneration.CSharp.Generators.ResultTypeGenerator + [global::System.CodeDom.Compiler.GeneratedCode("StrawberryShake", "11.0.0")] + public partial class GetBarsResult : global::System.IEquatable, IGetBarsResult + { + public GetBarsResult(global::System.Collections.Generic.IReadOnlyList? bars) + { + Bars = bars; + } + + public global::System.Collections.Generic.IReadOnlyList? Bars { get; } + + public virtual global::System.Boolean Equals(GetBarsResult? other) + { + if (ReferenceEquals(null, other)) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + if (other.GetType() != GetType()) + { + return false; + } + + return (global::StrawberryShake.Internal.ComparisonHelper.SequenceEqual(Bars, other.Bars)); + } + + public override global::System.Boolean Equals(global::System.Object? obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != GetType()) + { + return false; + } + + return Equals((GetBarsResult)obj); + } + + public override global::System.Int32 GetHashCode() + { + unchecked + { + int hash = 5; + if (Bars != null) + { + foreach (var Bars_elm in Bars) + { + if (Bars_elm != null) + { + hash ^= 397 * Bars_elm.GetHashCode(); + } + } + } + + return hash; + } + } + } + + // StrawberryShake.CodeGeneration.CSharp.Generators.ResultTypeGenerator + [global::System.CodeDom.Compiler.GeneratedCode("StrawberryShake", "11.0.0")] + public partial class GetBars_Bars_Bar : global::System.IEquatable, IGetBars_Bars_Bar + { + public GetBars_Bars_Bar(global::System.String id, global::System.String? name) + { + Id = id; + Name = name; + } + + public global::System.String Id { get; } + public global::System.String? Name { get; } + + public virtual global::System.Boolean Equals(GetBars_Bars_Bar? other) + { + if (ReferenceEquals(null, other)) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + if (other.GetType() != GetType()) + { + return false; + } + + return (Id.Equals(other.Id)) && ((Name is null && other.Name is null) || Name != null && Name.Equals(other.Name)); + } + + public override global::System.Boolean Equals(global::System.Object? obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != GetType()) + { + return false; + } + + return Equals((GetBars_Bars_Bar)obj); + } + + public override global::System.Int32 GetHashCode() + { + unchecked + { + int hash = 5; + hash ^= 397 * Id.GetHashCode(); + if (Name != null) + { + hash ^= 397 * Name.GetHashCode(); + } + + return hash; + } + } + } + + // StrawberryShake.CodeGeneration.CSharp.Generators.ResultInterfaceGenerator + [global::System.CodeDom.Compiler.GeneratedCode("StrawberryShake", "11.0.0")] + public partial interface IGetBarsResult + { + public global::System.Collections.Generic.IReadOnlyList? Bars { get; } + } + + // StrawberryShake.CodeGeneration.CSharp.Generators.ResultInterfaceGenerator + [global::System.CodeDom.Compiler.GeneratedCode("StrawberryShake", "11.0.0")] + public partial interface IGetBars_Bars + { + public global::System.String Id { get; } + public global::System.String? Name { get; } + } + + // StrawberryShake.CodeGeneration.CSharp.Generators.ResultInterfaceGenerator + [global::System.CodeDom.Compiler.GeneratedCode("StrawberryShake", "11.0.0")] + public partial interface IGetBars_Bars_Bar : IGetBars_Bars + { + } + + // StrawberryShake.CodeGeneration.CSharp.Generators.OperationDocumentGenerator + /// + /// Represents the operation service of the GetBars GraphQL operation + /// + /// query GetBars($a: String!, $b: String) { + /// bars(a: $a, b: $b) { + /// __typename + /// id + /// name + /// ... on Bar { + /// id + /// } + /// } + /// } + /// + /// + [global::System.CodeDom.Compiler.GeneratedCode("StrawberryShake", "11.0.0")] + public partial class GetBarsQueryDocument : global::StrawberryShake.IDocument + { + private GetBarsQueryDocument() + { + } + + public static GetBarsQueryDocument Instance { get; } = new GetBarsQueryDocument(); + public global::StrawberryShake.OperationKind Kind => global::StrawberryShake.OperationKind.Query; + public global::System.ReadOnlySpan Body => "query GetBars($a: String!, $b: String) { bars(a: $a, b: $b) { __typename id name ... on Bar { id } } }"u8; + public global::StrawberryShake.DocumentHash Hash { get; } = new global::StrawberryShake.DocumentHash("sha1Hash", "d77fc7854088f9716b48874b752a2d2b8be41aef"); + + public override global::System.String ToString() + { +#if NETCOREAPP3_1_OR_GREATER + return global::System.Text.Encoding.UTF8.GetString(Body); +#else + return global::System.Text.Encoding.UTF8.GetString(Body.ToArray()); +#endif + } + } + + // StrawberryShake.CodeGeneration.CSharp.Generators.OperationServiceGenerator + /// + /// Represents the operation service of the GetBars GraphQL operation + /// + /// query GetBars($a: String!, $b: String) { + /// bars(a: $a, b: $b) { + /// __typename + /// id + /// name + /// ... on Bar { + /// id + /// } + /// } + /// } + /// + /// + [global::System.CodeDom.Compiler.GeneratedCode("StrawberryShake", "11.0.0")] + public partial class GetBarsQuery : global::Foo.Bar.IGetBarsQuery + { + private readonly global::StrawberryShake.IOperationExecutor _operationExecutor; + private readonly global::StrawberryShake.Serialization.IInputValueFormatter _stringFormatter; + private readonly global::System.Collections.Immutable.ImmutableArray> _configure = global::System.Collections.Immutable.ImmutableArray>.Empty; + public GetBarsQuery(global::StrawberryShake.IOperationExecutor operationExecutor, global::StrawberryShake.Serialization.ISerializerResolver serializerResolver) + { + _operationExecutor = operationExecutor ?? throw new global::System.ArgumentNullException(nameof(operationExecutor)); + _stringFormatter = serializerResolver.GetInputValueFormatter("String"); + } + + private GetBarsQuery(global::StrawberryShake.IOperationExecutor operationExecutor, global::System.Collections.Immutable.ImmutableArray> configure, global::StrawberryShake.Serialization.IInputValueFormatter @stringFormatter) + { + _operationExecutor = operationExecutor; + _configure = configure; + _stringFormatter = @stringFormatter; + } + + global::System.Type global::StrawberryShake.IOperationRequestFactory.ResultType => typeof(IGetBarsResult); + + public global::Foo.Bar.IGetBarsQuery With(global::System.Action configure) + { + return new global::Foo.Bar.GetBarsQuery(_operationExecutor, _configure.Add(configure), _stringFormatter); + } + + public global::Foo.Bar.IGetBarsQuery WithRequestUri(global::System.Uri requestUri) + { + return With(r => r.ContextData["StrawberryShake.Transport.Http.HttpConnection.RequestUri"] = requestUri); + } + + public global::Foo.Bar.IGetBarsQuery WithHttpClient(global::System.Net.Http.HttpClient httpClient) + { + return With(r => r.ContextData["StrawberryShake.Transport.Http.HttpConnection.HttpClient"] = httpClient); + } + + public async global::System.Threading.Tasks.Task> ExecuteAsync(global::System.String a, global::System.String? b, global::System.Threading.CancellationToken cancellationToken = default) + { + var request = CreateRequest(a, b); + foreach (var configure in _configure) + { + configure(request); + } + + return await _operationExecutor.ExecuteAsync(request, cancellationToken).ConfigureAwait(false); + } + + public global::System.IObservable> Watch(global::System.String a, global::System.String? b, global::StrawberryShake.ExecutionStrategy? strategy = null) + { + var request = CreateRequest(a, b); + return _operationExecutor.Watch(request, strategy); + } + + private global::StrawberryShake.OperationRequest CreateRequest(global::System.String a, global::System.String? b) + { + var variables = new global::System.Collections.Generic.Dictionary(); + variables.Add("a", FormatA(a)); + variables.Add("b", FormatB(b)); + return CreateRequest(variables); + } + + private global::StrawberryShake.OperationRequest CreateRequest(global::System.Collections.Generic.IReadOnlyDictionary? variables) + { + return new global::StrawberryShake.OperationRequest(id: GetBarsQueryDocument.Instance.Hash.Value, name: "GetBars", document: GetBarsQueryDocument.Instance, strategy: global::StrawberryShake.RequestStrategy.Default, variables: variables); + } + + private global::System.Object? FormatA(global::System.String value) + { + if (value is null) + { + throw new global::System.ArgumentNullException(nameof(value)); + } + + return _stringFormatter.Format(value); + } + + private global::System.Object? FormatB(global::System.String? value) + { + if (value is null) + { + return value; + } + else + { + return _stringFormatter.Format(value); + } + } + + global::StrawberryShake.OperationRequest global::StrawberryShake.IOperationRequestFactory.Create(global::System.Collections.Generic.IReadOnlyDictionary? variables) + { + return CreateRequest(variables!); + } + } + + // StrawberryShake.CodeGeneration.CSharp.Generators.OperationServiceInterfaceGenerator + /// + /// Represents the operation service of the GetBars GraphQL operation + /// + /// query GetBars($a: String!, $b: String) { + /// bars(a: $a, b: $b) { + /// __typename + /// id + /// name + /// ... on Bar { + /// id + /// } + /// } + /// } + /// + /// + [global::System.CodeDom.Compiler.GeneratedCode("StrawberryShake", "11.0.0")] + public partial interface IGetBarsQuery : global::StrawberryShake.IOperationRequestFactory + { + global::Foo.Bar.IGetBarsQuery With(global::System.Action configure); + global::Foo.Bar.IGetBarsQuery WithRequestUri(global::System.Uri requestUri); + global::Foo.Bar.IGetBarsQuery WithHttpClient(global::System.Net.Http.HttpClient httpClient); + global::System.Threading.Tasks.Task> ExecuteAsync(global::System.String a, global::System.String? b, global::System.Threading.CancellationToken cancellationToken = default); + global::System.IObservable> Watch(global::System.String a, global::System.String? b, global::StrawberryShake.ExecutionStrategy? strategy = null); + } + + // StrawberryShake.CodeGeneration.CSharp.Generators.ClientGenerator + /// + /// Represents the FooClient GraphQL client + /// + [global::System.CodeDom.Compiler.GeneratedCode("StrawberryShake", "11.0.0")] + public partial class FooClient : global::Foo.Bar.IFooClient + { + private readonly global::Foo.Bar.IGetBarsQuery _getBars; + public FooClient(global::Foo.Bar.IGetBarsQuery getBars) + { + _getBars = getBars ?? throw new global::System.ArgumentNullException(nameof(getBars)); + } + + public static global::System.String ClientName => "FooClient"; + public global::Foo.Bar.IGetBarsQuery GetBars => _getBars; + } + + // StrawberryShake.CodeGeneration.CSharp.Generators.ClientInterfaceGenerator + /// + /// Represents the FooClient GraphQL client + /// + [global::System.CodeDom.Compiler.GeneratedCode("StrawberryShake", "11.0.0")] + public partial interface IFooClient + { + global::Foo.Bar.IGetBarsQuery GetBars { get; } + } +} + +namespace Foo.Bar.State +{ + // StrawberryShake.CodeGeneration.CSharp.Generators.EntityTypeGenerator + [global::System.CodeDom.Compiler.GeneratedCode("StrawberryShake", "0.0.0.0")] + public partial class BarEntity + { + public BarEntity(global::System.String id = default !, global::System.String? name = default !) + { + Id = id; + Name = name; + } + + public global::System.String Id { get; } + public global::System.String? Name { get; } + } + + // StrawberryShake.CodeGeneration.CSharp.Generators.ResultDataFactoryGenerator + [global::System.CodeDom.Compiler.GeneratedCode("StrawberryShake", "11.0.0")] + public partial class GetBarsResultFactory : global::StrawberryShake.IOperationResultDataFactory + { + private readonly global::StrawberryShake.IEntityStore _entityStore; + private readonly global::StrawberryShake.IEntityMapper _getBars_Bars_BarFromBarEntityMapper; + public GetBarsResultFactory(global::StrawberryShake.IEntityStore entityStore, global::StrawberryShake.IEntityMapper getBars_Bars_BarFromBarEntityMapper) + { + _entityStore = entityStore ?? throw new global::System.ArgumentNullException(nameof(entityStore)); + _getBars_Bars_BarFromBarEntityMapper = getBars_Bars_BarFromBarEntityMapper ?? throw new global::System.ArgumentNullException(nameof(getBars_Bars_BarFromBarEntityMapper)); + } + + global::System.Type global::StrawberryShake.IOperationResultDataFactory.ResultType => typeof(global::Foo.Bar.IGetBarsResult); + + public GetBarsResult Create(global::StrawberryShake.IOperationResultDataInfo dataInfo, global::StrawberryShake.IEntityStoreSnapshot? snapshot = null) + { + if (snapshot is null) + { + snapshot = _entityStore.CurrentSnapshot; + } + + if (dataInfo is GetBarsResultInfo info) + { + return new GetBarsResult(MapIGetBars_BarsArray(info.Bars, snapshot)); + } + + throw new global::System.ArgumentException("GetBarsResultInfo expected."); + } + + private global::System.Collections.Generic.IReadOnlyList? MapIGetBars_BarsArray(global::System.Collections.Generic.IReadOnlyList? list, global::StrawberryShake.IEntityStoreSnapshot snapshot) + { + if (list is null) + { + return null; + } + + var bars = new global::System.Collections.Generic.List(); + foreach (global::StrawberryShake.EntityId? child in list) + { + bars.Add(MapIGetBars_Bars(child, snapshot)); + } + + return bars; + } + + private global::Foo.Bar.IGetBars_Bars? MapIGetBars_Bars(global::StrawberryShake.EntityId? entityId, global::StrawberryShake.IEntityStoreSnapshot snapshot) + { + if (entityId is null) + { + return null; + } + + if (entityId.Value.Name.Equals("Bar", global::System.StringComparison.Ordinal)) + { + return _getBars_Bars_BarFromBarEntityMapper.Map(snapshot.GetEntity(entityId.Value) ?? throw new global::StrawberryShake.GraphQLClientException()); + } + + throw new global::System.NotSupportedException(); + } + + global::System.Object global::StrawberryShake.IOperationResultDataFactory.Create(global::StrawberryShake.IOperationResultDataInfo dataInfo, global::StrawberryShake.IEntityStoreSnapshot? snapshot) + { + return Create(dataInfo, snapshot); + } + } + + // StrawberryShake.CodeGeneration.CSharp.Generators.ResultInfoGenerator + [global::System.CodeDom.Compiler.GeneratedCode("StrawberryShake", "11.0.0")] + public partial class GetBarsResultInfo : global::StrawberryShake.IOperationResultDataInfo + { + private readonly global::System.Collections.Generic.IReadOnlyCollection _entityIds; + private readonly global::System.UInt64 _version; + public GetBarsResultInfo(global::System.Collections.Generic.IReadOnlyList? bars, global::System.Collections.Generic.IReadOnlyCollection entityIds, global::System.UInt64 version) + { + Bars = bars; + _entityIds = entityIds ?? throw new global::System.ArgumentNullException(nameof(entityIds)); + _version = version; + } + + public global::System.Collections.Generic.IReadOnlyList? Bars { get; } + public global::System.Collections.Generic.IReadOnlyCollection EntityIds => _entityIds; + public global::System.UInt64 Version => _version; + + public global::StrawberryShake.IOperationResultDataInfo WithVersion(global::System.UInt64 version) + { + return new GetBarsResultInfo(Bars, _entityIds, version); + } + } + + // StrawberryShake.CodeGeneration.CSharp.Generators.JsonResultBuilderGenerator + [global::System.CodeDom.Compiler.GeneratedCode("StrawberryShake", "11.0.0")] + public partial class GetBarsBuilder : global::StrawberryShake.OperationResultBuilder + { + private readonly global::StrawberryShake.IEntityStore _entityStore; + private readonly global::StrawberryShake.IEntityIdSerializer _idSerializer; + private readonly global::StrawberryShake.Serialization.ILeafValueParser _stringParser; + public GetBarsBuilder(global::StrawberryShake.IEntityStore entityStore, global::StrawberryShake.IEntityIdSerializer idSerializer, global::StrawberryShake.IOperationResultDataFactory resultDataFactory, global::StrawberryShake.Serialization.ISerializerResolver serializerResolver) + { + _entityStore = entityStore ?? throw new global::System.ArgumentNullException(nameof(entityStore)); + _idSerializer = idSerializer ?? throw new global::System.ArgumentNullException(nameof(idSerializer)); + ResultDataFactory = resultDataFactory ?? throw new global::System.ArgumentNullException(nameof(resultDataFactory)); + _stringParser = serializerResolver.GetLeafValueParser("String") ?? throw new global::System.ArgumentException("No serializer for type `String` found."); + } + + protected override global::StrawberryShake.IOperationResultDataFactory ResultDataFactory { get; } + protected override global::System.Boolean CapturePersistedData => true; + + protected override global::StrawberryShake.IOperationResultDataInfo BuildData(global::System.Text.Json.JsonElement obj) + { + var entityIds = new global::System.Collections.Generic.HashSet(); + global::StrawberryShake.IEntityStoreSnapshot snapshot = default !; + global::System.Collections.Generic.IReadOnlyList? barsId = default !; + _entityStore.Update(session => + { + barsId = Update_IGetBars_BarsEntityArray(session, global::StrawberryShake.Json.JsonElementExtensions.GetPropertyOrNull(obj, "bars"), entityIds); + snapshot = session.CurrentSnapshot; + }); + return new GetBarsResultInfo(barsId, entityIds, snapshot.Version); + } + + private global::System.Collections.Generic.IReadOnlyList? Update_IGetBars_BarsEntityArray(global::StrawberryShake.IEntityStoreUpdateSession session, global::System.Text.Json.JsonElement? obj, global::System.Collections.Generic.ISet entityIds) + { + if (!obj.HasValue) + { + return null; + } + + if (obj.Value.ValueKind == global::System.Text.Json.JsonValueKind.Null) + { + return null; + } + + var bars = new global::System.Collections.Generic.List(); + foreach (global::System.Text.Json.JsonElement child in obj.Value.EnumerateArray()) + { + bars.Add(Update_IGetBars_BarsEntity(session, child, entityIds)); + } + + return bars; + } + + private global::StrawberryShake.EntityId? Update_IGetBars_BarsEntity(global::StrawberryShake.IEntityStoreUpdateSession session, global::System.Text.Json.JsonElement? obj, global::System.Collections.Generic.ISet entityIds) + { + if (!obj.HasValue) + { + return null; + } + + if (obj.Value.ValueKind == global::System.Text.Json.JsonValueKind.Null) + { + return null; + } + + global::StrawberryShake.EntityId entityId = _idSerializer.Parse(obj.Value); + entityIds.Add(entityId); + if (entityId.Name.Equals("Bar", global::System.StringComparison.Ordinal)) + { + if (session.CurrentSnapshot.TryGetEntity(entityId, out global::Foo.Bar.State.BarEntity? entity)) + { + session.SetEntity(entityId, new global::Foo.Bar.State.BarEntity(Deserialize_NonNullableString(global::StrawberryShake.Json.JsonElementExtensions.GetPropertyOrNull(obj, "id")), Deserialize_String(global::StrawberryShake.Json.JsonElementExtensions.GetPropertyOrNull(obj, "name")))); + } + else + { + session.SetEntity(entityId, new global::Foo.Bar.State.BarEntity(Deserialize_NonNullableString(global::StrawberryShake.Json.JsonElementExtensions.GetPropertyOrNull(obj, "id")), Deserialize_String(global::StrawberryShake.Json.JsonElementExtensions.GetPropertyOrNull(obj, "name")))); + } + + return entityId; + } + + throw new global::System.NotSupportedException(); + } + + private global::System.String Deserialize_NonNullableString(global::System.Text.Json.JsonElement? obj) + { + if (!obj.HasValue) + { + throw new global::System.ArgumentNullException(); + } + + if (obj.Value.ValueKind == global::System.Text.Json.JsonValueKind.Null) + { + throw new global::System.ArgumentNullException(); + } + + return _stringParser.Parse(obj.Value.GetString()!); + } + + private global::System.String? Deserialize_String(global::System.Text.Json.JsonElement? obj) + { + if (!obj.HasValue) + { + return null; + } + + if (obj.Value.ValueKind == global::System.Text.Json.JsonValueKind.Null) + { + return null; + } + + return _stringParser.Parse(obj.Value.GetString()!); + } + } + + // StrawberryShake.CodeGeneration.CSharp.Generators.ResultFromEntityTypeMapperGenerator + [global::System.CodeDom.Compiler.GeneratedCode("StrawberryShake", "11.0.0")] + public partial class GetBars_Bars_BarFromBarEntityMapper : global::StrawberryShake.IEntityMapper + { + private readonly global::StrawberryShake.IEntityStore _entityStore; + public GetBars_Bars_BarFromBarEntityMapper(global::StrawberryShake.IEntityStore entityStore) + { + _entityStore = entityStore ?? throw new global::System.ArgumentNullException(nameof(entityStore)); + } + + public GetBars_Bars_Bar Map(global::Foo.Bar.State.BarEntity entity, global::StrawberryShake.IEntityStoreSnapshot? snapshot = null) + { + if (snapshot is null) + { + snapshot = _entityStore.CurrentSnapshot; + } + + return new GetBars_Bars_Bar(entity.Id, entity.Name); + } + } + + // StrawberryShake.CodeGeneration.CSharp.Generators.EntityIdFactoryGenerator + [global::System.CodeDom.Compiler.GeneratedCode("StrawberryShake", "11.0.0")] + public partial class FooClientEntityIdFactory : global::StrawberryShake.IEntityIdSerializer + { + private static readonly global::System.Text.Json.JsonWriterOptions _options = new global::System.Text.Json.JsonWriterOptions() + { + Indented = false + }; + public global::StrawberryShake.EntityId Parse(global::System.Text.Json.JsonElement obj) + { + global::System.String __typename = obj.GetProperty("__typename").GetString()!; + return __typename switch + { + "Bar" => ParseBarEntityId(obj, __typename), + _ => throw new global::System.NotSupportedException()}; + } + + public global::System.String Format(global::StrawberryShake.EntityId entityId) + { + return entityId.Name switch + { + "Bar" => FormatBarEntityId(entityId), + _ => throw new global::System.NotSupportedException()}; + } + + private global::StrawberryShake.EntityId ParseBarEntityId(global::System.Text.Json.JsonElement obj, global::System.String type) + { + return new global::StrawberryShake.EntityId(type, obj.GetProperty("id").GetString()!); + } + + private global::System.String FormatBarEntityId(global::StrawberryShake.EntityId entityId) + { + using var writer = new global::StrawberryShake.Internal.ArrayWriter(); + using var jsonWriter = new global::System.Text.Json.Utf8JsonWriter(writer, _options); + jsonWriter.WriteStartObject(); + jsonWriter.WriteString("__typename", entityId.Name); + jsonWriter.WriteString("id", (global::System.String)entityId.Value); + jsonWriter.WriteEndObject(); + jsonWriter.Flush(); + return global::System.Text.Encoding.UTF8.GetString(writer.GetInternalBuffer(), 0, writer.Length); + } + } + + // StrawberryShake.CodeGeneration.CSharp.Generators.StoreAccessorGenerator + [global::System.CodeDom.Compiler.GeneratedCode("StrawberryShake", "11.0.0")] + public partial class FooClientStoreAccessor : global::StrawberryShake.StoreAccessor + { + public FooClientStoreAccessor(global::StrawberryShake.IOperationStore operationStore, global::StrawberryShake.IEntityStore entityStore, global::StrawberryShake.IEntityIdSerializer entityIdSerializer, global::System.Collections.Generic.IEnumerable requestFactories, global::System.Collections.Generic.IEnumerable resultDataFactories) : base(operationStore, entityStore, entityIdSerializer, requestFactories, resultDataFactories) + { + } + } +} + +namespace Microsoft.Extensions.DependencyInjection +{ + // StrawberryShake.CodeGeneration.CSharp.Generators.DependencyInjectionGenerator + [global::System.CodeDom.Compiler.GeneratedCode("StrawberryShake", "11.0.0")] + public static partial class FooClientServiceCollectionExtensions + { + public static global::StrawberryShake.IClientBuilder AddFooClient(this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services, global::StrawberryShake.ExecutionStrategy strategy = global::StrawberryShake.ExecutionStrategy.NetworkOnly) + { + var serviceCollection = new global::Microsoft.Extensions.DependencyInjection.ServiceCollection(); + global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddSingleton(services, sp => + { + ConfigureClientDefault(sp, serviceCollection, strategy); + return new ClientServiceProvider(global::Microsoft.Extensions.DependencyInjection.ServiceCollectionContainerBuilderExtensions.BuildServiceProvider(serviceCollection)); + }); + global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddSingleton(services, sp => new global::Foo.Bar.State.FooClientStoreAccessor(global::Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService(global::Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService(sp)), global::Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService(global::Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService(sp)), global::Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService(global::Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService(sp)), global::Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService>(global::Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService(sp)), global::Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService>(global::Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService(sp)))); + global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddSingleton(services, sp => global::Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService(global::Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService(sp))); + global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddSingleton(services, sp => global::Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService(global::Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService(sp))); + global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddSingleton(services, sp => global::Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService(global::Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService(sp))); + global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddSingleton(services, sp => global::Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService(global::Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService(sp))); + return new global::StrawberryShake.ClientBuilder("FooClient", services, serviceCollection); + } + + private static global::Microsoft.Extensions.DependencyInjection.IServiceCollection ConfigureClientDefault(global::System.IServiceProvider parentServices, global::Microsoft.Extensions.DependencyInjection.ServiceCollection services, global::StrawberryShake.ExecutionStrategy strategy = global::StrawberryShake.ExecutionStrategy.NetworkOnly) + { + global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAddSingleton(services); + global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAddSingleton(services, sp => new global::StrawberryShake.OperationStore(global::Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService(sp))); + global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddSingleton(services, sp => + { + var clientFactory = global::Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService(parentServices); + return new global::StrawberryShake.Transport.Http.HttpConnection(() => clientFactory.CreateClient("FooClient")); + }); + global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddSingleton, global::Foo.Bar.State.GetBars_Bars_BarFromBarEntityMapper>(services); + global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddSingleton(services); + global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddSingleton(services); + global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddSingleton(services); + global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddSingleton(services); + global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddSingleton(services); + global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddSingleton(services); + global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddSingleton(services); + global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddSingleton(services); + global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddSingleton(services); + global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddSingleton(services); + global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddSingleton(services); + global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddSingleton(services); + global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddSingleton(services); + global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddSingleton(services); + global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddSingleton(services); + global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddSingleton(services); + global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddSingleton(services); + global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddSingleton(services); + global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddSingleton(services); + global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddSingleton(services); + global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddSingleton(services); + global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddSingleton(services); + global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddSingleton(services); + global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddSingleton(services); + global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddSingleton(services); + global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddSingleton(services, sp => new global::StrawberryShake.Serialization.SerializerResolver(global::System.Linq.Enumerable.Concat(global::Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService>(parentServices), global::Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService>(sp)))); + global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddSingleton, global::Foo.Bar.State.GetBarsResultFactory>(services); + global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddSingleton(services, sp => global::Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService>(sp)); + global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddSingleton(services, sp => global::Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService(sp)); + global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddSingleton, global::Foo.Bar.State.GetBarsBuilder>(services); + global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddSingleton>(services, sp => new global::StrawberryShake.OperationExecutor(global::Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService(sp), () => global::Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService>(sp), () => global::Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService>(sp), global::Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService(sp), strategy)); + global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddSingleton, global::StrawberryShake.Json.JsonResultPatcher>(services); + global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddSingleton(services); + global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddSingleton(services, sp => global::Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService(sp)); +#if NET10_0_OR_GREATER + global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddSingleton>, global::StrawberryShake.Razor.OperationResultPersistentStateSerializer>(services); +#endif + global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddSingleton(services); + global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddSingleton(services); + global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddSingleton(services, sp => global::Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService(sp)); + return services; + } + + private sealed class ClientServiceProvider : System.IServiceProvider, System.IDisposable + { + private readonly System.IServiceProvider _provider; + public ClientServiceProvider(System.IServiceProvider provider) + { + _provider = provider; + } + + public object? GetService(System.Type serviceType) + { + return _provider.GetService(serviceType); + } + + public void Dispose() + { + if (_provider is System.IDisposable d) + { + d.Dispose(); + } + } + } + } +} + + diff --git a/src/StrawberryShake/Tooling/src/Configuration/StrawberryShakeSettings.cs b/src/StrawberryShake/Tooling/src/Configuration/StrawberryShakeSettings.cs index 17ab5d5b27c..86d75e1cfd3 100644 --- a/src/StrawberryShake/Tooling/src/Configuration/StrawberryShakeSettings.cs +++ b/src/StrawberryShake/Tooling/src/Configuration/StrawberryShakeSettings.cs @@ -60,6 +60,12 @@ public class StrawberryShakeSettings /// public bool? RazorComponents { get; set; } + /// + /// Defines if generated operation results shall be persistable via the .NET 10 + /// [PersistentState] attribute. Requires a store. + /// + public bool? RazorPersistedState { get; set; } + /// /// Gets the record generator settings. /// diff --git a/src/StrawberryShake/Tooling/src/dotnet-graphql/GeneratorHelpers.cs b/src/StrawberryShake/Tooling/src/dotnet-graphql/GeneratorHelpers.cs index d88ccc2abca..01c669c1c51 100644 --- a/src/StrawberryShake/Tooling/src/dotnet-graphql/GeneratorHelpers.cs +++ b/src/StrawberryShake/Tooling/src/dotnet-graphql/GeneratorHelpers.cs @@ -81,6 +81,7 @@ public static CSharpGeneratorSettings CreateSettings( InputRecords = configSettings.Records.Inputs, EntityRecords = configSettings.Records.Entities, RazorComponents = configSettings.RazorComponents ?? args.RazorComponents, + RazorPersistedState = configSettings.RazorPersistedState ?? false, SingleCodeFile = configSettings.UseSingleFile ?? args.UseSingleFile, RequestStrategy = configSettings.RequestStrategy ?? args.Strategy, HashProvider = GetHashProvider(configSettings.HashAlgorithm ?? args.HashAlgorithm),