diff --git a/src/StrawberryShake/Client/src/Core/IOperationExecutor.cs b/src/StrawberryShake/Client/src/Core/IOperationExecutor.cs index ddee66031d6..d87b08e1285 100644 --- a/src/StrawberryShake/Client/src/Core/IOperationExecutor.cs +++ b/src/StrawberryShake/Client/src/Core/IOperationExecutor.cs @@ -39,4 +39,27 @@ Task> ExecuteAsync( IObservable> Watch( OperationRequest request, ExecutionStrategy? strategy = null); + + /// + /// Registers a request and subscribes to updates on the request results, optionally + /// seeding the store from a previously persisted transport payload so that the operation + /// is not re-executed. This is used to rehydrate prerendered Blazor components. + /// + /// + /// The operation request. + /// + /// + /// The UTF-8 encoded JSON of the GraphQL response "data" object captured during a server + /// prerender, or null to execute the operation normally. + /// + /// + /// The request execution strategy. + /// + /// + /// The observable that can be used to subscribe to results. + /// + IObservable> Watch( + OperationRequest request, + ReadOnlyMemory? persistedState, + ExecutionStrategy? strategy = null); } 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/OperationExecutor.cs b/src/StrawberryShake/Client/src/Core/OperationExecutor.cs index 1ae1a338e83..75799d3485d 100644 --- a/src/StrawberryShake/Client/src/Core/OperationExecutor.cs +++ b/src/StrawberryShake/Client/src/Core/OperationExecutor.cs @@ -117,4 +117,47 @@ public IObservable> Watch( request, strategy ?? _strategy); } + + /// + /// Registers a request and subscribes to updates on the request results, optionally + /// seeding the store from a previously persisted transport payload so that the operation + /// is not re-executed. This is used to rehydrate prerendered Blazor components. + /// + /// + /// The operation request. + /// + /// + /// The UTF-8 encoded JSON of the GraphQL response "data" object captured during a server + /// prerender, or null to execute the operation normally. + /// + /// + /// The request execution strategy. + /// + /// + /// The observable that can be used to subscribe to results. + /// + public IObservable> Watch( + OperationRequest request, + ReadOnlyMemory? persistedState, + ExecutionStrategy? strategy = null) + { + ArgumentNullException.ThrowIfNull(request); + + if (persistedState is { } state) + { + // Rehydrate the persisted payload through the existing deserialization path so + // the entity store and operation store are seeded, then serve it from the cache. + var result = _resultBuilder().BuildFromPersistedData(state); + _operationStore.Set(request, result); + strategy ??= ExecutionStrategy.CacheFirst; + } + + return new OperationExecutorObservable( + _connection, + _operationStore, + _resultBuilder, + _resultPatcher, + request, + strategy ?? ExecutionStrategy.CacheFirst); + } } 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/StorelessOperationExecutor.cs b/src/StrawberryShake/Client/src/Core/StorelessOperationExecutor.cs index d99464f63ef..0891197f97d 100644 --- a/src/StrawberryShake/Client/src/Core/StorelessOperationExecutor.cs +++ b/src/StrawberryShake/Client/src/Core/StorelessOperationExecutor.cs @@ -101,4 +101,16 @@ public IObservable> Watch( return new StorelessOperationExecutorObservable(_connection, _resultBuilder, request); } + + /// + /// Not supported by the storeless executor. Persisted component state requires a store + /// to rehydrate into. + /// + public IObservable> Watch( + OperationRequest request, + ReadOnlyMemory? persistedState, + ExecutionStrategy? strategy = null) + => throw new NotSupportedException( + "Persisted component state requires a client with a store. " + + "Disable NoStore to use persisted Razor components."); } 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/UsePersistentQuery.cs b/src/StrawberryShake/Client/src/Razor/UsePersistentQuery.cs new file mode 100644 index 00000000000..6ba67a499f7 --- /dev/null +++ b/src/StrawberryShake/Client/src/Razor/UsePersistentQuery.cs @@ -0,0 +1,230 @@ +using System.Buffers; +using System.Text.Json; +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Rendering; + +namespace StrawberryShake.Razor; + +/// +/// A base class for generated Blazor query components that survive the prerender to +/// interactive boundary. During a server prerender the latest result is saved through +/// ; on the interactive client the saved payload is +/// taken back and rehydrated through the store so the query is not executed a second time. +/// +/// +/// The operation result type. +/// +public abstract class UsePersistentQuery : ComponentBase, IDisposable where TResult : class +{ + private readonly TaskCompletionSource _firstResult = + new(TaskCreationOptions.RunContinuationsAsynchronously); + private IDisposable? _subscription; + private PersistingComponentStateSubscription _persistingSubscription; + private bool _registeredPersisting; + private string? _subscribedKey; + private JsonElement? _persistedData; + private bool _isInitializing = true; + private bool _isErrorResult; + private bool _isSuccessResult; + private TResult? _result; + private IReadOnlyList? _errors; + private bool _disposed; + + [Inject] + internal PersistentComponentState PersistentComponentState { get; set; } = default!; + + [Parameter] public RenderFragment? ChildContent { get; set; } + + [Parameter] public RenderFragment>? ErrorContent { get; set; } + + [Parameter] public RenderFragment? LoadingContent { get; set; } + + [Parameter] public EventCallback> OnOperationResult { get; set; } + + /// + /// Gets a stable persistence key for the current operation arguments. The same arguments + /// must produce the same key on the server prerender and on the interactive client. + /// + protected abstract string GetPersistenceKey(); + + /// + /// Creates the watch observable for the current operation arguments. When + /// is provided the store is seeded from it and the + /// result is served from the cache without a network call. + /// + /// + /// The persisted transport "data" payload, or null to execute normally. + /// + protected abstract IObservable> CreateWatch( + ReadOnlyMemory? persistedState); + + /// + protected override async Task OnInitializedAsync() + { + var key = GetPersistenceKey(); + _subscribedKey = key; + + if (PersistentComponentState.TryTakeFromJson(key, out var persisted)) + { + // Interactive render after a server prerender: rehydrate the persisted payload + // and serve it from the store without a network call. + Subscribe(CreateWatch(ToUtf8(persisted))); + return; + } + + // Cold render (server prerender or non-prerendered): register a callback that saves + // the latest payload so the interactive client can rehydrate it, then execute. + _persistingSubscription = PersistentComponentState.RegisterOnPersisting(PersistAsync); + _registeredPersisting = true; + + Subscribe(CreateWatch(persistedState: null)); + + // Await the first result so prerendered HTML contains data instead of the loading + // state and the persisting callback has a payload to save. + await _firstResult.Task; + } + + /// + protected override void OnParametersSet() + { + // Re-subscribe only when the operation arguments actually change. This supports + // changing parameters at runtime while avoiding a re-subscribe on every re-render + // (for example when a parent passes fresh render-fragment delegates). + var key = GetPersistenceKey(); + + if (_subscribedKey is not null + && !string.Equals(_subscribedKey, key, StringComparison.Ordinal)) + { + _subscribedKey = key; + Subscribe(CreateWatch(persistedState: null)); + } + } + + private Task PersistAsync() + { + if (_persistedData is { } data) + { + PersistentComponentState.PersistAsJson(GetPersistenceKey(), data); + } + + return Task.CompletedTask; + } + + /// + /// Subscribes the component to the specified result observable, disposing any previous + /// subscription. + /// + protected void Subscribe(IObservable> observable) + { + _subscription?.Dispose(); + _subscription = observable.Subscribe(new Observer(this)); + } + + private void OnNext(IOperationResult operationResult) + { + _result = operationResult.Data; + _errors = operationResult.Errors; + _isErrorResult = operationResult.IsErrorResult(); + _isSuccessResult = operationResult.IsSuccessResult(); + _isInitializing = false; + + if (operationResult.ContextData.TryGetValue(WellKnownContextData.PersistedData, out var payload) + && payload is JsonElement element) + { + _persistedData = element; + } + + _firstResult.TrySetResult(); + InvokeAsync(StateHasChanged); + OnOperationResult.InvokeAsync(operationResult); + } + + private void OnError(Exception error) + { + _errors = new IClientError[] { new ClientError(error.Message, exception: error) }; + _isErrorResult = true; + _isSuccessResult = false; + _isInitializing = false; + _firstResult.TrySetResult(); + InvokeAsync(StateHasChanged); + } + + /// + protected override void BuildRenderTree(RenderTreeBuilder builder) + { + if (_isInitializing && LoadingContent is not null) + { + builder.AddContent(0, LoadingContent); + } + + if (_isErrorResult) + { + builder.AddContent(0, ErrorContent, _errors!); + } + + if (_isSuccessResult) + { + builder.AddContent(0, ChildContent, _result!); + } + + base.BuildRenderTree(builder); + } + + private static ReadOnlyMemory ToUtf8(JsonElement element) + { + var bufferWriter = new ArrayBufferWriter(); + + using (var writer = new Utf8JsonWriter(bufferWriter)) + { + element.WriteTo(writer); + } + + return bufferWriter.WrittenMemory; + } + + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Releases the subscriptions held by the component. + /// + protected virtual void Dispose(bool disposing) + { + if (!_disposed) + { + if (disposing) + { + _subscription?.Dispose(); + + if (_registeredPersisting) + { + _persistingSubscription.Dispose(); + } + } + + _disposed = true; + } + } + + private sealed class Observer : IObserver> + { + private readonly UsePersistentQuery _component; + + public Observer(UsePersistentQuery component) + { + _component = component; + } + + public void OnCompleted() + { + } + + public void OnError(Exception error) => _component.OnError(error); + + public void OnNext(IOperationResult value) => _component.OnNext(value); + } +} diff --git a/src/StrawberryShake/Client/test/Core.Tests/OperationExecutorTests.cs b/src/StrawberryShake/Client/test/Core.Tests/OperationExecutorTests.cs index 25c6b9c9d07..2562412422d 100644 --- a/src/StrawberryShake/Client/test/Core.Tests/OperationExecutorTests.cs +++ b/src/StrawberryShake/Client/test/Core.Tests/OperationExecutorTests.cs @@ -269,6 +269,62 @@ public async Task Watch_CacheFirst_ValueNotInCache() Assert.Equal(storeUpdateResult, actualStoreUpdateResult.Data); } + [Fact] + public async Task Watch_WithPersistedState_RehydratesWithoutNetwork() + { + // arrange + var connection = new Mock>(); + var operationStore = new Mock(); + var resultBuilder = new PersistedMockResultBuilder(); + var resultPatcher = new Mock>(); + var document = new Mock(); + var request = new OperationRequest("abc", document.Object); + var observer = new ResultObserver(); + + var executor = new OperationExecutor( + connection.Object, + () => resultBuilder, + () => resultPatcher.Object, + operationStore.Object); + + var rehydrated = Mock.Of>(e => e.Data == "rehydrated"); + operationStore.Setup(e => e.TryGet(request, out rehydrated)).Returns(true); + operationStore.Setup(e => e.Watch(request)) + .Returns(Observable.Return(rehydrated)); + + // act + executor.Watch(request, ReadOnlyMemory.Empty, strategy: null) + .Subscribe(observer); + + // assert + var result = await observer.WaitForResult(); + Assert.Equal("rehydrated", result.Data); + operationStore.Verify( + e => e.Set(request, It.IsAny>()), + Times.Once); + connection.Verify(e => e.ExecuteAsync(It.IsAny()), Times.Never); + } + + [Fact] + public void Storeless_Watch_WithPersistedState_Throws() + { + // arrange + var connection = new Mock>(); + var resultBuilder = new PersistedMockResultBuilder(); + var resultPatcher = new Mock>(); + var document = new Mock(); + var request = new OperationRequest("abc", document.Object); + + var executor = new StorelessOperationExecutor( + connection.Object, + () => resultBuilder, + () => resultPatcher.Object); + + // act & assert + Assert.Throws( + () => executor.Watch(request, ReadOnlyMemory.Empty, strategy: null)); + } + private static async IAsyncEnumerable ToAsyncEnumerable(params TValue[] values) { foreach (var value in values) @@ -287,6 +343,19 @@ public IOperationResult Build(Response response) } } + private sealed class PersistedMockResultBuilder : IOperationResultBuilder + { + public IOperationResult Build(Response response) + { + return Mock.Of>(e => e.Data == response.Body); + } + + public IOperationResult BuildFromPersistedData(ReadOnlyMemory persistedData) + { + return Mock.Of>(e => e.Data == "rehydrated"); + } + } + private sealed class ResultObserver : IObserver> { private static readonly TimeSpan s_timeout = TimeSpan.FromMilliseconds(500); 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/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..fce3f964a2a 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; } + /// + /// Generate razor query components that persist their result during a server prerender + /// and rehydrate it on the interactive client (requires a store). + /// + public bool RazorPersistedState { get; set; } + /// /// Generate a single CSharp code file. /// diff --git a/src/StrawberryShake/CodeGeneration/src/CodeGeneration.CSharp/Generators/JsonResultBuilderGenerator.cs b/src/StrawberryShake/CodeGeneration/src/CodeGeneration.CSharp/Generators/JsonResultBuilderGenerator.cs index c10f15b0359..657482a918c 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 persisted Razor components can save it + // during a server prerender and rehydrate it on the interactive client. + 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.CSharp/Generators/OperationServiceGenerator.cs b/src/StrawberryShake/CodeGeneration/src/CodeGeneration.CSharp/Generators/OperationServiceGenerator.cs index 86b0f60af75..b8d04a0a55a 100644 --- a/src/StrawberryShake/CodeGeneration/src/CodeGeneration.CSharp/Generators/OperationServiceGenerator.cs +++ b/src/StrawberryShake/CodeGeneration/src/CodeGeneration.CSharp/Generators/OperationServiceGenerator.cs @@ -25,6 +25,7 @@ public class OperationServiceGenerator : ClassBaseGenerator private const string Request = "request"; private const string Value = "value"; private const string CancellationToken = "cancellationToken"; + private const string PersistedState = "persistedState"; private static readonly string s_filesType = TypeNames.Dictionary.WithGeneric(TypeNames.String, TypeNames.Upload.MakeNullable()); @@ -116,6 +117,17 @@ protected override void Generate( AddFileMapMethods(descriptor, classBuilder); classBuilder.AddMethod(CreateWatchMethod(descriptor, resultTypeName)); + + // Persisted component state is a query-only, store-backed feature. + if (settings.RazorPersistedState + && !settings.NoStore + && descriptor is QueryOperationDescriptor) + { + classBuilder.AddMethod( + CreateWatchWithPersistedStateMethod(descriptor, resultTypeName)); + classBuilder.AddMethod(CreatePersistenceKeyMethod(descriptor)); + } + classBuilder.AddMethod(CreateRequestMethod(descriptor)); classBuilder.AddMethod(CreateRequestVariablesMethod(descriptor, descriptor.HasUpload)); @@ -332,6 +344,82 @@ private static MethodBuilder CreateWatchMethod( .AddArgument(Strategy)); } + private static MethodBuilder CreateWatchWithPersistedStateMethod( + OperationDescriptor descriptor, + string runtimeTypeName) + { + var watchMethod = + MethodBuilder + .New() + .SetPublic() + .SetReturnType( + TypeNames.IOperationObservable + .WithGeneric(TypeNames.IOperationResult.WithGeneric(runtimeTypeName))) + .SetName(TypeNames.Watch); + + foreach (var arg in descriptor.Arguments) + { + watchMethod + .AddParameter() + .SetName(GetParameterName(arg.Name)) + .SetType(arg.Type.ToTypeReference()); + } + + watchMethod.AddParameter() + .SetName(PersistedState) + .SetType("global::System.ReadOnlyMemory?"); + + watchMethod.AddParameter() + .SetName(Strategy) + .SetType(TypeNames.ExecutionStrategy.MakeNullable()) + .SetDefault("null"); + + return watchMethod + .AddCode( + AssignmentBuilder + .New() + .SetLeftHandSide($"var {Request}") + .SetRightHandSide(CreateRequestMethodCall(descriptor))) + .AddCode( + MethodCallBuilder + .New() + .SetReturn() + .SetMethodName(UnderscoreOperationExecutor, "Watch") + .AddArgument(Request) + .AddArgument(PersistedState) + .AddArgument(Strategy)); + } + + private static MethodBuilder CreatePersistenceKeyMethod(OperationDescriptor descriptor) + { + var method = + MethodBuilder + .New() + .SetPublic() + .SetReturnType(TypeNames.String) + .SetName("GetPersistenceKey"); + + foreach (var arg in descriptor.Arguments) + { + method + .AddParameter() + .SetName(GetParameterName(arg.Name)) + .SetType(arg.Type.ToTypeReference()); + } + + return method + .AddCode( + AssignmentBuilder + .New() + .SetLeftHandSide($"var {Request}") + .SetRightHandSide(CreateRequestMethodCall(descriptor))) + .AddCode( + MethodCallBuilder + .New() + .SetReturn() + .SetMethodName(Request, "GetHash")); + } + private MethodBuilder CreateExecuteMethod( OperationDescriptor operationDescriptor, string runtimeTypeName) diff --git a/src/StrawberryShake/CodeGeneration/src/CodeGeneration.CSharp/Generators/RazorQueryGenerator.cs b/src/StrawberryShake/CodeGeneration/src/CodeGeneration.CSharp/Generators/RazorQueryGenerator.cs index 1b99c1627f9..a2b81606043 100644 --- a/src/StrawberryShake/CodeGeneration/src/CodeGeneration.CSharp/Generators/RazorQueryGenerator.cs +++ b/src/StrawberryShake/CodeGeneration/src/CodeGeneration.CSharp/Generators/RazorQueryGenerator.cs @@ -27,9 +27,16 @@ protected override CSharpSyntaxGeneratorResult Generate( ? SyntaxKind.PublicKeyword : SyntaxKind.InternalKeyword; + // Persisted component state requires a store to rehydrate into. + var persistedState = settings.RazorPersistedState && !settings.NoStore; + + var baseType = persistedState + ? TypeNames.UsePersistentQuery.WithGeneric(resultType) + : TypeNames.UseQuery.WithGeneric(resultType); + var classDeclaration = ClassDeclaration(componentName) - .AddImplements(TypeNames.UseQuery.WithGeneric(resultType)) + .AddImplements(baseType) .AddModifiers( Token(modifier), Token(SyntaxKind.PartialKeyword)) @@ -42,10 +49,20 @@ protected override CSharpSyntaxGeneratorResult Generate( CreateArgumentProperty(argument)); } - classDeclaration = classDeclaration.AddMembers( - CreateLifecycleMethodMethod("OnInitialized", descriptor.Arguments)); - classDeclaration = classDeclaration.AddMembers( - CreateLifecycleMethodMethod("OnParametersSet", descriptor.Arguments)); + if (persistedState) + { + classDeclaration = classDeclaration.AddMembers( + CreatePersistenceKeyMethod(descriptor.Arguments)); + classDeclaration = classDeclaration.AddMembers( + CreateWatchMethod(resultType, descriptor.Arguments)); + } + else + { + classDeclaration = classDeclaration.AddMembers( + CreateLifecycleMethodMethod("OnInitialized", descriptor.Arguments)); + classDeclaration = classDeclaration.AddMembers( + CreateLifecycleMethodMethod("OnParametersSet", descriptor.Arguments)); + } return new CSharpSyntaxGeneratorResult( componentName, @@ -130,4 +147,77 @@ private MethodDeclarationSyntax CreateLifecycleMethodMethod( Token(SyntaxKind.OverrideKeyword))) .WithBody(Block(bodyStatements)); } + + private MethodDeclarationSyntax CreatePersistenceKeyMethod( + IReadOnlyList arguments) + { + var argumentList = new List(); + + foreach (var argument in arguments) + { + argumentList.Add(Argument(IdentifierName(GetPropertyName(argument.Name)))); + } + + var bodyStatements = + SingletonList( + ReturnStatement( + InvocationExpression( + MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + IdentifierName("Operation"), + IdentifierName("GetPersistenceKey"))) + .WithArgumentList( + ArgumentList(SeparatedList(argumentList))))); + + return MethodDeclaration( + ParseTypeName(TypeNames.String), + Identifier("GetPersistenceKey")) + .WithModifiers( + TokenList( + Token(SyntaxKind.ProtectedKeyword), + Token(SyntaxKind.OverrideKeyword))) + .WithBody(Block(bodyStatements)); + } + + private MethodDeclarationSyntax CreateWatchMethod( + string resultType, + IReadOnlyList arguments) + { + var argumentList = new List(); + + foreach (var argument in arguments) + { + argumentList.Add(Argument(IdentifierName(GetPropertyName(argument.Name)))); + } + + argumentList.Add(Argument(IdentifierName("persistedState"))); + + var bodyStatements = + SingletonList( + ReturnStatement( + InvocationExpression( + MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + IdentifierName("Operation"), + IdentifierName("Watch"))) + .WithArgumentList( + ArgumentList(SeparatedList(argumentList))))); + + var returnType = + ParseTypeName( + "global::System.IObservable<" + + TypeNames.IOperationResult.WithGeneric(resultType) + + ">"); + + return MethodDeclaration(returnType, Identifier("CreateWatch")) + .WithModifiers( + TokenList( + Token(SyntaxKind.ProtectedKeyword), + Token(SyntaxKind.OverrideKeyword))) + .AddParameterListParameters( + Parameter(Identifier("persistedState")) + .WithType( + ParseTypeName("global::System.ReadOnlyMemory?"))) + .WithBody(Block(bodyStatements)); + } } diff --git a/src/StrawberryShake/CodeGeneration/src/CodeGeneration/CSharpSyntaxGeneratorSettings.cs b/src/StrawberryShake/CodeGeneration/src/CodeGeneration/CSharpSyntaxGeneratorSettings.cs index 45039d1f034..85a87ec07d4 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,10 @@ public CSharpSyntaxGeneratorSettings( /// Generate Razor components. /// public bool RazorComponents { get; } + + /// + /// Generate Razor query components that persist their result during a server prerender + /// and rehydrate it on the interactive client (requires a store). + /// + public bool RazorPersistedState { get; } } diff --git a/src/StrawberryShake/CodeGeneration/src/CodeGeneration/TypeNames.cs b/src/StrawberryShake/CodeGeneration/src/CodeGeneration/TypeNames.cs index 4b24031ce79..8decd43bba0 100644 --- a/src/StrawberryShake/CodeGeneration/src/CodeGeneration/TypeNames.cs +++ b/src/StrawberryShake/CodeGeneration/src/CodeGeneration/TypeNames.cs @@ -243,6 +243,9 @@ public static class TypeNames public const string UseSubscription = StrawberryShakeNamespace + "Razor." + nameof(UseSubscription); + public const string UsePersistentQuery = + StrawberryShakeNamespace + "Razor." + nameof(UsePersistentQuery); + 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..c88e5b301f2 100644 --- a/src/StrawberryShake/CodeGeneration/test/CodeGeneration.Razor.Tests/RazorGeneratorTests.cs +++ b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.Razor.Tests/RazorGeneratorTests.cs @@ -44,4 +44,44 @@ type Bar { """, "extend schema @key(fields: \"id\")"); } + + [Fact] + public void Query_With_Persisted_State() + { + // force assembly to load! + Assert.NotNull(typeof(UsePersistentQuery<>)); + + AssertResult( + settings: new() { RazorComponents = true, RazorPersistedState = true }, + """ + query GetBars($a: String! $b: String) { + bars(a: $a b: $b) { + id + name + } + } + + mutation SaveBars($a: String! $b: String) { + saveBar(a: $a b: $b) { + id + name + } + } + """, + """ + type Query { + bars(a: String!, b: String): [Bar] + } + + type Mutation { + saveBar(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..c4cad40764f --- /dev/null +++ b/src/StrawberryShake/CodeGeneration/test/CodeGeneration.Razor.Tests/__snapshots__/RazorGeneratorTests.Query_With_Persisted_State.snap @@ -0,0 +1,1354 @@ +// 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.ResultTypeGenerator + [global::System.CodeDom.Compiler.GeneratedCode("StrawberryShake", "11.0.0")] + public partial class SaveBarsResult : global::System.IEquatable, ISaveBarsResult + { + public SaveBarsResult(global::Foo.Bar.ISaveBars_SaveBar? saveBar) + { + SaveBar = saveBar; + } + + public global::Foo.Bar.ISaveBars_SaveBar? SaveBar { get; } + + public virtual global::System.Boolean Equals(SaveBarsResult? other) + { + if (ReferenceEquals(null, other)) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + if (other.GetType() != GetType()) + { + return false; + } + + return (((SaveBar is null && other.SaveBar is null) || SaveBar != null && SaveBar.Equals(other.SaveBar))); + } + + 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((SaveBarsResult)obj); + } + + public override global::System.Int32 GetHashCode() + { + unchecked + { + int hash = 5; + if (SaveBar != null) + { + hash ^= 397 * SaveBar.GetHashCode(); + } + + return hash; + } + } + } + + // StrawberryShake.CodeGeneration.CSharp.Generators.ResultTypeGenerator + [global::System.CodeDom.Compiler.GeneratedCode("StrawberryShake", "11.0.0")] + public partial class SaveBars_SaveBar_Bar : global::System.IEquatable, ISaveBars_SaveBar_Bar + { + public SaveBars_SaveBar_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(SaveBars_SaveBar_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((SaveBars_SaveBar_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 ISaveBarsResult + { + public global::Foo.Bar.ISaveBars_SaveBar? SaveBar { get; } + } + + // StrawberryShake.CodeGeneration.CSharp.Generators.ResultInterfaceGenerator + [global::System.CodeDom.Compiler.GeneratedCode("StrawberryShake", "11.0.0")] + public partial interface ISaveBars_SaveBar + { + 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 ISaveBars_SaveBar_Bar : ISaveBars_SaveBar + { + } + + // 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); + } + + public global::System.IObservable> Watch(global::System.String a, global::System.String? b, global::System.ReadOnlyMemory? persistedState, global::StrawberryShake.ExecutionStrategy? strategy = null) + { + var request = CreateRequest(a, b); + return _operationExecutor.Watch(request, persistedState, strategy); + } + + public global::System.String GetPersistenceKey(global::System.String a, global::System.String? b) + { + var request = CreateRequest(a, b); + return request.GetHash(); + } + + 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.OperationDocumentGenerator + /// + /// Represents the operation service of the SaveBars GraphQL operation + /// + /// mutation SaveBars($a: String!, $b: String) { + /// saveBar(a: $a, b: $b) { + /// __typename + /// id + /// name + /// ... on Bar { + /// id + /// } + /// } + /// } + /// + /// + [global::System.CodeDom.Compiler.GeneratedCode("StrawberryShake", "11.0.0")] + public partial class SaveBarsMutationDocument : global::StrawberryShake.IDocument + { + private SaveBarsMutationDocument() + { + } + + public static SaveBarsMutationDocument Instance { get; } = new SaveBarsMutationDocument(); + public global::StrawberryShake.OperationKind Kind => global::StrawberryShake.OperationKind.Mutation; + public global::System.ReadOnlySpan Body => "mutation SaveBars($a: String!, $b: String) { saveBar(a: $a, b: $b) { __typename id name ... on Bar { id } } }"u8; + public global::StrawberryShake.DocumentHash Hash { get; } = new global::StrawberryShake.DocumentHash("sha1Hash", "46ced9034dad969a5b30d75b8a91366be7ad75b5"); + + 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 SaveBars GraphQL operation + /// + /// mutation SaveBars($a: String!, $b: String) { + /// saveBar(a: $a, b: $b) { + /// __typename + /// id + /// name + /// ... on Bar { + /// id + /// } + /// } + /// } + /// + /// + [global::System.CodeDom.Compiler.GeneratedCode("StrawberryShake", "11.0.0")] + public partial class SaveBarsMutation : global::Foo.Bar.ISaveBarsMutation + { + 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 SaveBarsMutation(global::StrawberryShake.IOperationExecutor operationExecutor, global::StrawberryShake.Serialization.ISerializerResolver serializerResolver) + { + _operationExecutor = operationExecutor ?? throw new global::System.ArgumentNullException(nameof(operationExecutor)); + _stringFormatter = serializerResolver.GetInputValueFormatter("String"); + } + + private SaveBarsMutation(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(ISaveBarsResult); + + public global::Foo.Bar.ISaveBarsMutation With(global::System.Action configure) + { + return new global::Foo.Bar.SaveBarsMutation(_operationExecutor, _configure.Add(configure), _stringFormatter); + } + + public global::Foo.Bar.ISaveBarsMutation WithRequestUri(global::System.Uri requestUri) + { + return With(r => r.ContextData["StrawberryShake.Transport.Http.HttpConnection.RequestUri"] = requestUri); + } + + public global::Foo.Bar.ISaveBarsMutation 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: SaveBarsMutationDocument.Instance.Hash.Value, name: "SaveBars", document: SaveBarsMutationDocument.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 SaveBars GraphQL operation + /// + /// mutation SaveBars($a: String!, $b: String) { + /// saveBar(a: $a, b: $b) { + /// __typename + /// id + /// name + /// ... on Bar { + /// id + /// } + /// } + /// } + /// + /// + [global::System.CodeDom.Compiler.GeneratedCode("StrawberryShake", "11.0.0")] + public partial interface ISaveBarsMutation : global::StrawberryShake.IOperationRequestFactory + { + global::Foo.Bar.ISaveBarsMutation With(global::System.Action configure); + global::Foo.Bar.ISaveBarsMutation WithRequestUri(global::System.Uri requestUri); + global::Foo.Bar.ISaveBarsMutation 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; + private readonly global::Foo.Bar.ISaveBarsMutation _saveBars; + public FooClient(global::Foo.Bar.IGetBarsQuery getBars, global::Foo.Bar.ISaveBarsMutation saveBars) + { + _getBars = getBars ?? throw new global::System.ArgumentNullException(nameof(getBars)); + _saveBars = saveBars ?? throw new global::System.ArgumentNullException(nameof(saveBars)); + } + + public static global::System.String ClientName => "FooClient"; + public global::Foo.Bar.IGetBarsQuery GetBars => _getBars; + public global::Foo.Bar.ISaveBarsMutation SaveBars => _saveBars; + } + + // 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; } + + global::Foo.Bar.ISaveBarsMutation SaveBars { 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.ResultDataFactoryGenerator + [global::System.CodeDom.Compiler.GeneratedCode("StrawberryShake", "11.0.0")] + public partial class SaveBarsResultFactory : global::StrawberryShake.IOperationResultDataFactory + { + private readonly global::StrawberryShake.IEntityStore _entityStore; + private readonly global::StrawberryShake.IEntityMapper _saveBars_SaveBar_BarFromBarEntityMapper; + public SaveBarsResultFactory(global::StrawberryShake.IEntityStore entityStore, global::StrawberryShake.IEntityMapper saveBars_SaveBar_BarFromBarEntityMapper) + { + _entityStore = entityStore ?? throw new global::System.ArgumentNullException(nameof(entityStore)); + _saveBars_SaveBar_BarFromBarEntityMapper = saveBars_SaveBar_BarFromBarEntityMapper ?? throw new global::System.ArgumentNullException(nameof(saveBars_SaveBar_BarFromBarEntityMapper)); + } + + global::System.Type global::StrawberryShake.IOperationResultDataFactory.ResultType => typeof(global::Foo.Bar.ISaveBarsResult); + + public SaveBarsResult Create(global::StrawberryShake.IOperationResultDataInfo dataInfo, global::StrawberryShake.IEntityStoreSnapshot? snapshot = null) + { + if (snapshot is null) + { + snapshot = _entityStore.CurrentSnapshot; + } + + if (dataInfo is SaveBarsResultInfo info) + { + return new SaveBarsResult(MapISaveBars_SaveBar(info.SaveBar, snapshot)); + } + + throw new global::System.ArgumentException("SaveBarsResultInfo expected."); + } + + private global::Foo.Bar.ISaveBars_SaveBar? MapISaveBars_SaveBar(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 _saveBars_SaveBar_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 SaveBarsResultInfo : global::StrawberryShake.IOperationResultDataInfo + { + private readonly global::System.Collections.Generic.IReadOnlyCollection _entityIds; + private readonly global::System.UInt64 _version; + public SaveBarsResultInfo(global::StrawberryShake.EntityId? saveBar, global::System.Collections.Generic.IReadOnlyCollection entityIds, global::System.UInt64 version) + { + SaveBar = saveBar; + _entityIds = entityIds ?? throw new global::System.ArgumentNullException(nameof(entityIds)); + _version = version; + } + + public global::StrawberryShake.EntityId? SaveBar { 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 SaveBarsResultInfo(SaveBar, _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.JsonResultBuilderGenerator + [global::System.CodeDom.Compiler.GeneratedCode("StrawberryShake", "11.0.0")] + public partial class SaveBarsBuilder : global::StrawberryShake.OperationResultBuilder + { + private readonly global::StrawberryShake.IEntityStore _entityStore; + private readonly global::StrawberryShake.IEntityIdSerializer _idSerializer; + private readonly global::StrawberryShake.Serialization.ILeafValueParser _stringParser; + public SaveBarsBuilder(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::StrawberryShake.EntityId? saveBarId = default !; + _entityStore.Update(session => + { + saveBarId = Update_ISaveBars_SaveBarEntity(session, global::StrawberryShake.Json.JsonElementExtensions.GetPropertyOrNull(obj, "saveBar"), entityIds); + snapshot = session.CurrentSnapshot; + }); + return new SaveBarsResultInfo(saveBarId, entityIds, snapshot.Version); + } + + private global::StrawberryShake.EntityId? Update_ISaveBars_SaveBarEntity(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.ResultFromEntityTypeMapperGenerator + [global::System.CodeDom.Compiler.GeneratedCode("StrawberryShake", "11.0.0")] + public partial class SaveBars_SaveBar_BarFromBarEntityMapper : global::StrawberryShake.IEntityMapper + { + private readonly global::StrawberryShake.IEntityStore _entityStore; + public SaveBars_SaveBar_BarFromBarEntityMapper(global::StrawberryShake.IEntityStore entityStore) + { + _entityStore = entityStore ?? throw new global::System.ArgumentNullException(nameof(entityStore)); + } + + public SaveBars_SaveBar_Bar Map(global::Foo.Bar.State.BarEntity entity, global::StrawberryShake.IEntityStoreSnapshot? snapshot = null) + { + if (snapshot is null) + { + snapshot = _entityStore.CurrentSnapshot; + } + + return new SaveBars_SaveBar_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))); + 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, global::Foo.Bar.State.SaveBars_SaveBar_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)); + global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddSingleton, global::Foo.Bar.State.SaveBarsResultFactory>(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.SaveBarsBuilder>(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)); + 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(); + } + } + } + } +} + + +// FooClient + +// +#nullable enable annotations +#nullable disable warnings + +namespace Foo.Bar.Components +{ + // StrawberryShake.CodeGeneration.CSharp.Generators.RazorQueryGenerator + [global::System.CodeDom.Compiler.GeneratedCode("StrawberryShake", "0.0.0.0")] + public partial class UseGetBars : global::StrawberryShake.Razor.UsePersistentQuery + { + [global::Microsoft.AspNetCore.Components.InjectAttribute] + internal global::Foo.Bar.GetBarsQuery Operation { get; set; } = default !; + + [global::Microsoft.AspNetCore.Components.ParameterAttribute] + public global::System.String A { get; set; } = default !; + + [global::Microsoft.AspNetCore.Components.ParameterAttribute] + public global::System.String? B { get; set; } + + protected override global::System.String GetPersistenceKey() + { + return Operation.GetPersistenceKey(A, B); + } + + protected override global::System.IObservable> CreateWatch(global::System.ReadOnlyMemory? persistedState) + { + return Operation.Watch(A, B, persistedState); + } + } +} + + diff --git a/src/StrawberryShake/Tooling/src/Configuration/StrawberryShakeSettings.cs b/src/StrawberryShake/Tooling/src/Configuration/StrawberryShakeSettings.cs index 17ab5d5b27c..9ab42770191 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 blazor query components shall persist their result during a + /// server prerender and rehydrate it on the interactive client. 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),