diff --git a/src/HotChocolate/Core/src/Execution/Processing/OperationCompiler.Context.cs b/src/HotChocolate/Core/src/Execution/Processing/OperationCompiler.Context.cs index e1610527272..f3ab6d4a72d 100644 --- a/src/HotChocolate/Core/src/Execution/Processing/OperationCompiler.Context.cs +++ b/src/HotChocolate/Core/src/Execution/Processing/OperationCompiler.Context.cs @@ -78,8 +78,10 @@ private class CompilerContext public bool IsInternalSelection { get; } - public IDictionary IncludeConditionLookup - { get; } + public IDictionary IncludeConditionLookup + { + get; + } public IImmutableList Optimizers { get; } @@ -93,6 +95,7 @@ public void RegisterFragment(IFragment fragment) { _fragments = new List(); } + _fragments.Add(fragment); } @@ -105,7 +108,7 @@ public void TryBranch(ObjectType type, ISelection selection) { SelectionSetNode selectionSet = selection.SelectionSet!; - if(!_processed.Add((selectionSet, type.Name))) + if (!_processed.Add((selectionSet, type.Name))) { return; } @@ -118,8 +121,7 @@ public void TryBranch(ObjectType type, ISelection selection) _variantsLookup[selectionSet] = selectionVariants; } - var context = new CompilerContext - ( + var context = new CompilerContext( type, Path.Push(selection.Field), selectionSet, @@ -145,8 +147,7 @@ public CompilerContext Branch(FragmentInfo fragment) _variantsLookup[fragment.SelectionSet] = selectionVariants; } - var context = new CompilerContext - ( + var context = new CompilerContext( Type, Path, fragment.SelectionSet, @@ -172,8 +173,7 @@ public CompilerContext Branch(FragmentInfo fragment) var rootSelections = new SelectionVariants(selectionSet); selectionVariantsLookup[selectionSet] = rootSelections; - var context = new CompilerContext - ( + var context = new CompilerContext( backlog, type, selectionSet, diff --git a/src/HotChocolate/Core/src/Execution/Processing/Selection.cs b/src/HotChocolate/Core/src/Execution/Processing/Selection.cs index df2488b9966..ecdc1c3e247 100644 --- a/src/HotChocolate/Core/src/Execution/Processing/Selection.cs +++ b/src/HotChocolate/Core/src/Execution/Processing/Selection.cs @@ -9,7 +9,7 @@ namespace HotChocolate.Execution.Processing { - public sealed class Selection : ISelection + public class Selection : ISelection { private static readonly ArgumentMap _emptyArguments = new ArgumentMap(new Dictionary()); @@ -56,6 +56,21 @@ public sealed class Selection : ISelection } } + public Selection(Selection selection) + { + _includeConditions = selection._includeConditions; + _selections = selection._selections; + _isReadOnly = selection._isReadOnly; + DeclaringType = selection.DeclaringType; + Field = selection.Field; + SyntaxNode = selection.SyntaxNode; + SyntaxNodes = selection.SyntaxNodes; + ResponseName = selection.ResponseName; + ResolverPipeline = selection.ResolverPipeline; + Arguments = selection.Arguments; + InclusionKind = selection.InclusionKind; + } + /// public IObjectType DeclaringType { get; } @@ -189,8 +204,7 @@ internal void MakeReadOnly() return first; } - return new FieldNode - ( + return new FieldNode( first.Location, first.Name, first.Alias, @@ -219,8 +233,7 @@ internal void MakeReadOnly() } } - return new SelectionSetNode - ( + return new SelectionSetNode( selections[0].SelectionSet!.Location, children ); @@ -269,7 +282,7 @@ internal void MakeReadOnly() private void ModifyCondition(bool hasConditions) => InclusionKind = (InclusionKind == SelectionInclusionKind.Internal - || InclusionKind == SelectionInclusionKind.InternalConditional) + || InclusionKind == SelectionInclusionKind.InternalConditional) ? (hasConditions ? SelectionInclusionKind.InternalConditional : SelectionInclusionKind.Internal) diff --git a/src/HotChocolate/Core/src/Execution/ThrowHelper.cs b/src/HotChocolate/Core/src/Execution/ThrowHelper.cs index 1a745d5b6c9..38d79108479 100644 --- a/src/HotChocolate/Core/src/Execution/ThrowHelper.cs +++ b/src/HotChocolate/Core/src/Execution/ThrowHelper.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using HotChocolate.Execution.Properties; using HotChocolate.Language; using static HotChocolate.Execution.Properties.Resources; diff --git a/src/HotChocolate/Core/src/Types/Types/Descriptors/Contracts/IObjectFieldDescriptor.cs b/src/HotChocolate/Core/src/Types/Types/Descriptors/Contracts/IObjectFieldDescriptor.cs index a8caeee3c3d..bbdf820747d 100644 --- a/src/HotChocolate/Core/src/Types/Types/Descriptors/Contracts/IObjectFieldDescriptor.cs +++ b/src/HotChocolate/Core/src/Types/Types/Descriptors/Contracts/IObjectFieldDescriptor.cs @@ -86,5 +86,7 @@ IObjectFieldDescriptor Directive() IObjectFieldDescriptor Directive( NameString name, params ArgumentNode[] arguments); + + IObjectFieldDescriptor ConfigureContextData(Action configure); } } diff --git a/src/HotChocolate/Core/src/Types/Types/Descriptors/Contracts/IObjectTypeDescriptor~1.cs b/src/HotChocolate/Core/src/Types/Types/Descriptors/Contracts/IObjectTypeDescriptor~1.cs index 9f401581fee..339b46867b2 100644 --- a/src/HotChocolate/Core/src/Types/Types/Descriptors/Contracts/IObjectTypeDescriptor~1.cs +++ b/src/HotChocolate/Core/src/Types/Types/Descriptors/Contracts/IObjectTypeDescriptor~1.cs @@ -48,7 +48,7 @@ public interface IObjectTypeDescriptor /// IObjectTypeDescriptor BindFields(BindingBehavior behavior); - /// + /// /// Defines that all fields have to be specified explicitly. /// IObjectTypeDescriptor BindFieldsExplicitly(); diff --git a/src/HotChocolate/Core/src/Types/Types/Descriptors/DescriptorBase~1.cs b/src/HotChocolate/Core/src/Types/Types/Descriptors/DescriptorBase~1.cs index 264112c6dcf..aa8e1d0ecd3 100644 --- a/src/HotChocolate/Core/src/Types/Types/Descriptors/DescriptorBase~1.cs +++ b/src/HotChocolate/Core/src/Types/Types/Descriptors/DescriptorBase~1.cs @@ -46,6 +46,11 @@ public T CreateDefinition() return Definition; } + public void ConfigureContextData(Action configure) + { + configure(Definition.ContextData); + } + protected virtual void OnCreateDefinition(T definition) { } @@ -88,7 +93,7 @@ private void OnBeforeCreate(Action configure) OnBeforeNaming(configure); private INamedDependencyDescriptor OnBeforeNaming( - Action configure) + Action configure) { if (configure is null) { diff --git a/src/HotChocolate/Core/src/Types/Types/Descriptors/ObjectFieldDescriptor.cs b/src/HotChocolate/Core/src/Types/Types/Descriptors/ObjectFieldDescriptor.cs index c015fe81864..0ae68f380e3 100644 --- a/src/HotChocolate/Core/src/Types/Types/Descriptors/ObjectFieldDescriptor.cs +++ b/src/HotChocolate/Core/src/Types/Types/Descriptors/ObjectFieldDescriptor.cs @@ -46,13 +46,15 @@ public class ObjectFieldDescriptor Definition.Member = member ?? throw new ArgumentNullException(nameof(member)); Definition.Name = context.Naming.GetMemberName( - member, MemberKind.ObjectField); + member, + MemberKind.ObjectField); Definition.Description = context.Naming.GetMemberDescription( - member, MemberKind.ObjectField); + member, + MemberKind.ObjectField); Definition.Type = context.TypeInspector.GetOutputReturnTypeRef(member); Definition.SourceType = sourceType; Definition.ResolverType = resolverType == sourceType ? null : resolverType; - + if (context.Naming.IsDeprecated(member, out var reason)) { Deprecated(reason); @@ -60,8 +62,9 @@ public class ObjectFieldDescriptor if (member is MethodInfo m) { - Parameters = m.GetParameters().ToDictionary( - t => new NameString(t.Name)); + Parameters = m + .GetParameters() + .ToDictionary(t => new NameString(t.Name)); Definition.ResultType = m.ReturnType; } else if (member is PropertyInfo p) @@ -87,9 +90,11 @@ public class ObjectFieldDescriptor if (member is { }) { Definition.Name = context.Naming.GetMemberName( - member, MemberKind.ObjectField); + member, + MemberKind.ObjectField); Definition.Description = context.Naming.GetMemberDescription( - member, MemberKind.ObjectField); + member, + MemberKind.ObjectField); Definition.Type = context.TypeInspector.GetOutputReturnTypeRef(member); if (context.Naming.IsDeprecated(member, out string? reason)) @@ -127,7 +132,6 @@ public class ObjectFieldDescriptor protected override void OnCreateDefinition( ObjectFieldDefinition definition) { - if (Definition.Member is { }) { Context.TypeInspector.ApplyAttributes( @@ -175,7 +179,7 @@ public new IObjectFieldDescriptor Name(NameString value) [Obsolete("Use `Deprecated`.")] public IObjectFieldDescriptor DeprecationReason(string? reason) => - Deprecated(reason); + Deprecated(reason); public new IObjectFieldDescriptor Deprecated(string? reason) { @@ -375,6 +379,13 @@ public new IObjectFieldDescriptor Directive() return this; } + public new IObjectFieldDescriptor ConfigureContextData( + Action configure) + { + base.ConfigureContextData(configure); + return this; + } + public static ObjectFieldDescriptor New( IDescriptorContext context, NameString fieldName) => diff --git a/src/HotChocolate/Core/src/Types/Types/Pagination/PagingMiddleware.cs b/src/HotChocolate/Core/src/Types/Types/Pagination/PagingMiddleware.cs index a6addc23587..e4c71a26265 100644 --- a/src/HotChocolate/Core/src/Types/Types/Pagination/PagingMiddleware.cs +++ b/src/HotChocolate/Core/src/Types/Types/Pagination/PagingMiddleware.cs @@ -1,10 +1,6 @@ using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; using System.Threading.Tasks; using HotChocolate.Resolvers; -using static HotChocolate.Utilities.ThrowHelper; #nullable enable @@ -31,8 +27,9 @@ public async Task InvokeAsync(IMiddlewareContext context) if (context.Result is not null) { - context.Result = - await _pagingHandler.SliceAsync(context, context.Result).ConfigureAwait(false); + context.Result = await _pagingHandler + .SliceAsync(context, context.Result) + .ConfigureAwait(false); } } } diff --git a/src/HotChocolate/Data/HotChocolate.Data.sln b/src/HotChocolate/Data/HotChocolate.Data.sln index 301e4bb6eec..fb143a31df9 100644 --- a/src/HotChocolate/Data/HotChocolate.Data.sln +++ b/src/HotChocolate/Data/HotChocolate.Data.sln @@ -62,6 +62,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HotChocolate.Data.Filters.S EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HotChocolate.Data.Sorting.Tests", "test\Data.Sorting.Tests\HotChocolate.Data.Sorting.Tests.csproj", "{A7269FAC-8C91-46BA-B292-221E8DE32BD3}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HotChocolate.Data.Projections.SqlServer.Tests", "test\Data.Projections.SqlServer.Tests\HotChocolate.Data.Projections.SqlServer.Tests.csproj", "{F9223FFE-BEA4-4C68-BF92-339FD9643084}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HotChocolate.Types.CursorPagination", "..\Core\src\Types.CursorPagination\HotChocolate.Types.CursorPagination.csproj", "{982D42FD-6E4D-41DC-8892-785BAA55294C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HotChocolate.Types.OffsetPagination", "..\Core\src\Types.OffsetPagination\HotChocolate.Types.OffsetPagination.csproj", "{1BC63911-799F-4189-801A-2B7C97FD2F23}" +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HotChocolate.Data.Sorting.InMemory.Tests", "test\Data.Sorting.InMemory.Tests\HotChocolate.Data.Sorting.InMemory.Tests.csproj", "{E5E0D684-4FF6-4124-8ADB-9E96A3FDA6FA}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HotChocolate.Data.Sorting.SqlLite.Tests", "test\Data.Sorting.SqlLite.Tests\HotChocolate.Data.Sorting.SqlLite.Tests.csproj", "{D445A9BB-D068-496A-B261-609799F95915}" @@ -104,8 +110,11 @@ Global {CCCD4A40-EA4F-4AF7-91A7-3D8DC14D186C} = {4EE990B2-C327-46DA-8FE8-F95AC228E47F} {9B858A08-741A-4C11-A243-3739C4E57B15} = {4EE990B2-C327-46DA-8FE8-F95AC228E47F} {A7269FAC-8C91-46BA-B292-221E8DE32BD3} = {4EE990B2-C327-46DA-8FE8-F95AC228E47F} - {E5E0D684-4FF6-4124-8ADB-9E96A3FDA6FA} = {4EE990B2-C327-46DA-8FE8-F95AC228E47F} + {F9223FFE-BEA4-4C68-BF92-339FD9643084} = {4EE990B2-C327-46DA-8FE8-F95AC228E47F} + {982D42FD-6E4D-41DC-8892-785BAA55294C} = {882EC02D-5E1D-41F5-AD9F-AA06E31D133A} + {1BC63911-799F-4189-801A-2B7C97FD2F23} = {882EC02D-5E1D-41F5-AD9F-AA06E31D133A} {D445A9BB-D068-496A-B261-609799F95915} = {4EE990B2-C327-46DA-8FE8-F95AC228E47F} + {E5E0D684-4FF6-4124-8ADB-9E96A3FDA6FA} = {4EE990B2-C327-46DA-8FE8-F95AC228E47F} {0E4E8E6F-A65C-42C0-BE9F-DFA002E59303} = {91887A91-7B1C-4287-A1E0-BD4E0DAF24C7} {16FB6511-AE94-46C4-9652-2665C6816546} = {4EE990B2-C327-46DA-8FE8-F95AC228E47F} EndGlobalSection @@ -362,18 +371,42 @@ Global {A7269FAC-8C91-46BA-B292-221E8DE32BD3}.Release|x64.Build.0 = Release|Any CPU {A7269FAC-8C91-46BA-B292-221E8DE32BD3}.Release|x86.ActiveCfg = Release|Any CPU {A7269FAC-8C91-46BA-B292-221E8DE32BD3}.Release|x86.Build.0 = Release|Any CPU - {E5E0D684-4FF6-4124-8ADB-9E96A3FDA6FA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {E5E0D684-4FF6-4124-8ADB-9E96A3FDA6FA}.Debug|Any CPU.Build.0 = Debug|Any CPU - {E5E0D684-4FF6-4124-8ADB-9E96A3FDA6FA}.Debug|x64.ActiveCfg = Debug|Any CPU - {E5E0D684-4FF6-4124-8ADB-9E96A3FDA6FA}.Debug|x64.Build.0 = Debug|Any CPU - {E5E0D684-4FF6-4124-8ADB-9E96A3FDA6FA}.Debug|x86.ActiveCfg = Debug|Any CPU - {E5E0D684-4FF6-4124-8ADB-9E96A3FDA6FA}.Debug|x86.Build.0 = Debug|Any CPU - {E5E0D684-4FF6-4124-8ADB-9E96A3FDA6FA}.Release|Any CPU.ActiveCfg = Release|Any CPU - {E5E0D684-4FF6-4124-8ADB-9E96A3FDA6FA}.Release|Any CPU.Build.0 = Release|Any CPU - {E5E0D684-4FF6-4124-8ADB-9E96A3FDA6FA}.Release|x64.ActiveCfg = Release|Any CPU - {E5E0D684-4FF6-4124-8ADB-9E96A3FDA6FA}.Release|x64.Build.0 = Release|Any CPU - {E5E0D684-4FF6-4124-8ADB-9E96A3FDA6FA}.Release|x86.ActiveCfg = Release|Any CPU - {E5E0D684-4FF6-4124-8ADB-9E96A3FDA6FA}.Release|x86.Build.0 = Release|Any CPU + {F9223FFE-BEA4-4C68-BF92-339FD9643084}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F9223FFE-BEA4-4C68-BF92-339FD9643084}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F9223FFE-BEA4-4C68-BF92-339FD9643084}.Debug|x64.ActiveCfg = Debug|Any CPU + {F9223FFE-BEA4-4C68-BF92-339FD9643084}.Debug|x64.Build.0 = Debug|Any CPU + {F9223FFE-BEA4-4C68-BF92-339FD9643084}.Debug|x86.ActiveCfg = Debug|Any CPU + {F9223FFE-BEA4-4C68-BF92-339FD9643084}.Debug|x86.Build.0 = Debug|Any CPU + {F9223FFE-BEA4-4C68-BF92-339FD9643084}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F9223FFE-BEA4-4C68-BF92-339FD9643084}.Release|Any CPU.Build.0 = Release|Any CPU + {F9223FFE-BEA4-4C68-BF92-339FD9643084}.Release|x64.ActiveCfg = Release|Any CPU + {F9223FFE-BEA4-4C68-BF92-339FD9643084}.Release|x64.Build.0 = Release|Any CPU + {F9223FFE-BEA4-4C68-BF92-339FD9643084}.Release|x86.ActiveCfg = Release|Any CPU + {F9223FFE-BEA4-4C68-BF92-339FD9643084}.Release|x86.Build.0 = Release|Any CPU + {982D42FD-6E4D-41DC-8892-785BAA55294C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {982D42FD-6E4D-41DC-8892-785BAA55294C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {982D42FD-6E4D-41DC-8892-785BAA55294C}.Debug|x64.ActiveCfg = Debug|Any CPU + {982D42FD-6E4D-41DC-8892-785BAA55294C}.Debug|x64.Build.0 = Debug|Any CPU + {982D42FD-6E4D-41DC-8892-785BAA55294C}.Debug|x86.ActiveCfg = Debug|Any CPU + {982D42FD-6E4D-41DC-8892-785BAA55294C}.Debug|x86.Build.0 = Debug|Any CPU + {982D42FD-6E4D-41DC-8892-785BAA55294C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {982D42FD-6E4D-41DC-8892-785BAA55294C}.Release|Any CPU.Build.0 = Release|Any CPU + {982D42FD-6E4D-41DC-8892-785BAA55294C}.Release|x64.ActiveCfg = Release|Any CPU + {982D42FD-6E4D-41DC-8892-785BAA55294C}.Release|x64.Build.0 = Release|Any CPU + {982D42FD-6E4D-41DC-8892-785BAA55294C}.Release|x86.ActiveCfg = Release|Any CPU + {982D42FD-6E4D-41DC-8892-785BAA55294C}.Release|x86.Build.0 = Release|Any CPU + {1BC63911-799F-4189-801A-2B7C97FD2F23}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1BC63911-799F-4189-801A-2B7C97FD2F23}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1BC63911-799F-4189-801A-2B7C97FD2F23}.Debug|x64.ActiveCfg = Debug|Any CPU + {1BC63911-799F-4189-801A-2B7C97FD2F23}.Debug|x64.Build.0 = Debug|Any CPU + {1BC63911-799F-4189-801A-2B7C97FD2F23}.Debug|x86.ActiveCfg = Debug|Any CPU + {1BC63911-799F-4189-801A-2B7C97FD2F23}.Debug|x86.Build.0 = Debug|Any CPU + {1BC63911-799F-4189-801A-2B7C97FD2F23}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1BC63911-799F-4189-801A-2B7C97FD2F23}.Release|Any CPU.Build.0 = Release|Any CPU + {1BC63911-799F-4189-801A-2B7C97FD2F23}.Release|x64.ActiveCfg = Release|Any CPU + {1BC63911-799F-4189-801A-2B7C97FD2F23}.Release|x64.Build.0 = Release|Any CPU + {1BC63911-799F-4189-801A-2B7C97FD2F23}.Release|x86.ActiveCfg = Release|Any CPU + {1BC63911-799F-4189-801A-2B7C97FD2F23}.Release|x86.Build.0 = Release|Any CPU {D445A9BB-D068-496A-B261-609799F95915}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D445A9BB-D068-496A-B261-609799F95915}.Debug|Any CPU.Build.0 = Debug|Any CPU {D445A9BB-D068-496A-B261-609799F95915}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -386,6 +419,8 @@ Global {D445A9BB-D068-496A-B261-609799F95915}.Release|x64.Build.0 = Release|Any CPU {D445A9BB-D068-496A-B261-609799F95915}.Release|x86.ActiveCfg = Release|Any CPU {D445A9BB-D068-496A-B261-609799F95915}.Release|x86.Build.0 = Release|Any CPU + {E5E0D684-4FF6-4124-8ADB-9E96A3FDA6FA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E5E0D684-4FF6-4124-8ADB-9E96A3FDA6FA}.Debug|Any CPU.Build.0 = Debug|Any CPU {0E4E8E6F-A65C-42C0-BE9F-DFA002E59303}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {0E4E8E6F-A65C-42C0-BE9F-DFA002E59303}.Debug|Any CPU.Build.0 = Debug|Any CPU {0E4E8E6F-A65C-42C0-BE9F-DFA002E59303}.Debug|x64.ActiveCfg = Debug|Any CPU diff --git a/src/HotChocolate/Data/src/Data/DataResources.Designer.cs b/src/HotChocolate/Data/src/Data/DataResources.Designer.cs index 322f6eb96fd..800b6e20224 100644 --- a/src/HotChocolate/Data/src/Data/DataResources.Designer.cs +++ b/src/HotChocolate/Data/src/Data/DataResources.Designer.cs @@ -326,5 +326,65 @@ internal class DataResources { return ResourceManager.GetString("FilterVisitor_InvalidField", resourceCulture); } } + + internal static string ProjectionProvider_NoConfigurationSpecified { + get { + return ResourceManager.GetString("ProjectionProvider_NoConfigurationSpecified", resourceCulture); + } + } + + internal static string ProjectionProvider_UnableToCreateFieldHandler { + get { + return ResourceManager.GetString("ProjectionProvider_UnableToCreateFieldHandler", resourceCulture); + } + } + + internal static string ProjectionProvider_NoHandlersConfigured { + get { + return ResourceManager.GetString("ProjectionProvider_NoHandlersConfigured", resourceCulture); + } + } + + internal static string ProjectionConvention_NoConfigurationSpecified { + get { + return ResourceManager.GetString("ProjectionConvention_NoConfigurationSpecified", resourceCulture); + } + } + + internal static string ProjectionConvention_NoProviderFound { + get { + return ResourceManager.GetString("ProjectionConvention_NoProviderFound", resourceCulture); + } + } + + internal static string ProjectionConventionDescriptor_MustImplementIProjectionProvider { + get { + return ResourceManager.GetString("ProjectionConventionDescriptor_MustImplementIProjectionProvider", resourceCulture); + } + } + + internal static string ProjectionProvider_CreateMoreThanOneError { + get { + return ResourceManager.GetString("ProjectionProvider_CreateMoreThanOneError", resourceCulture); + } + } + + internal static string ProjectionProvider_CouldNotProjectFiltering { + get { + return ResourceManager.GetString("ProjectionProvider_CouldNotProjectFiltering", resourceCulture); + } + } + + internal static string ProjectionProvider_CouldNotProjectSorting { + get { + return ResourceManager.GetString("ProjectionProvider_CouldNotProjectSorting", resourceCulture); + } + } + + internal static string ProjectionConvention_CouldNotProject { + get { + return ResourceManager.GetString("ProjectionConvention_CouldNotProject", resourceCulture); + } + } } } diff --git a/src/HotChocolate/Data/src/Data/DataResources.resx b/src/HotChocolate/Data/src/Data/DataResources.resx index 4b20ea25e2f..7af63bb972e 100644 --- a/src/HotChocolate/Data/src/Data/DataResources.resx +++ b/src/HotChocolate/Data/src/Data/DataResources.resx @@ -1,7 +1,8 @@ - + @@ -13,10 +14,14 @@ 1.3 - System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, + PublicKeyToken=b77a5c561934e089 + - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, + PublicKeyToken=b77a5c561934e089 + Unable to resolve the field runtime type for `{0}.{1}`. @@ -159,4 +164,34 @@ Filter visitor encountered invalid field + + No configuration was specified. + + + Unable to create field handler `{0}` for projection convention `{1}`. + + + The projection convention `{0}` does not specify and field handler. + + + No configuration was specified. + + + There is no provider defined for the projection convention `{0}`. + + + A projection provider has to implement `IProjectionProvider`. + + + Sequence contains more than one element. + + + Could not project filters on field + + + Could not project sorting on field + + + Projection Visitor is in invalid state. Projection failed! + diff --git a/src/HotChocolate/Data/src/Data/ErrorHelper.cs b/src/HotChocolate/Data/src/Data/ErrorHelper.cs index 67c507ba5f0..aab020c30f0 100644 --- a/src/HotChocolate/Data/src/Data/ErrorHelper.cs +++ b/src/HotChocolate/Data/src/Data/ErrorHelper.cs @@ -1,8 +1,10 @@ using System; using System.Linq; using HotChocolate.Data.Filters; +using HotChocolate.Data.Projections; using HotChocolate.Data.Sorting; using HotChocolate.Language; +using HotChocolate.Resolvers; using HotChocolate.Types; namespace HotChocolate.Data @@ -100,5 +102,39 @@ internal static class ErrorHelper .SetExtension("sortType", sortType.Visualize()) .Build(); } + + public static ISchemaError ProjectionConvention_UnableToCreateFieldHandler( + IProjectionProvider convention, + Type fieldHandler) => + SchemaErrorBuilder.New() + .SetMessage( + DataResources.FilterProvider_UnableToCreateFieldHandler, + fieldHandler.FullName ?? fieldHandler.Name, + convention.GetType().FullName ?? convention.GetType().Name) + .SetExtension(nameof(convention), convention) + .SetExtension(nameof(fieldHandler), fieldHandler) + .Build(); + + public static IError ProjectionProvider_CreateMoreThanOneError(IResolverContext context) => + ErrorBuilder.New() + .SetMessage(DataResources.ProjectionProvider_CreateMoreThanOneError) + .SetCode("SELECTIONS_SINGLE_MORE_THAN_ONE") + .SetPath(context.Path) + .AddLocation(context.FieldSelection) + .Build(); + + public static IError ProjectionProvider_CouldNotProjectFiltering( + IValueNode node) => + ErrorBuilder.New() + .SetMessage(DataResources.ProjectionProvider_CouldNotProjectFiltering) + .AddLocation(node) + .Build(); + + public static IError ProjectionProvider_CouldNotProjectSorting( + IValueNode node) => + ErrorBuilder.New() + .SetMessage(DataResources.ProjectionProvider_CouldNotProjectSorting) + .AddLocation(node) + .Build(); } } diff --git a/src/HotChocolate/Data/src/Data/Filters/Convention/FilterConvention.cs b/src/HotChocolate/Data/src/Data/Filters/Convention/FilterConvention.cs index 97fb89b0b56..45e8861a7b9 100644 --- a/src/HotChocolate/Data/src/Data/Filters/Convention/FilterConvention.cs +++ b/src/HotChocolate/Data/src/Data/Filters/Convention/FilterConvention.cs @@ -8,6 +8,7 @@ using HotChocolate.Resolvers; using HotChocolate.Types; using HotChocolate.Types.Descriptors; +using HotChocolate.Types.Descriptors.Definitions; using HotChocolate.Utilities; using static HotChocolate.Data.DataResources; using static HotChocolate.Data.ThrowHelper; @@ -198,6 +199,9 @@ public NameString GetOperationName(int operation) public FieldMiddleware CreateExecutor() => _provider.CreateExecutor(_argumentName); + public virtual void ConfigureField(IObjectFieldDescriptor descriptor) => + _provider.ConfigureField(_argumentName, descriptor); + public bool TryGetHandler( ITypeDiscoveryContext context, IFilterInputTypeDefinition typeDefinition, diff --git a/src/HotChocolate/Data/src/Data/Filters/Convention/IFilterConvention.cs b/src/HotChocolate/Data/src/Data/Filters/Convention/IFilterConvention.cs index f0222c6f930..f1b8bcd40e6 100644 --- a/src/HotChocolate/Data/src/Data/Filters/Convention/IFilterConvention.cs +++ b/src/HotChocolate/Data/src/Data/Filters/Convention/IFilterConvention.cs @@ -3,6 +3,7 @@ using System.Reflection; using HotChocolate.Configuration; using HotChocolate.Resolvers; +using HotChocolate.Types; using HotChocolate.Types.Descriptors; namespace HotChocolate.Data.Filters @@ -65,7 +66,7 @@ public interface IFilterConvention : IConvention /// The member from which a field shall be inferred. /// /// - /// Returns a that represents the field type. + /// Returns a that represents the field type. /// ExtendedTypeReference GetFieldType(MemberInfo member); @@ -130,5 +131,14 @@ public interface IFilterConvention : IConvention /// for the specified entity type. /// FieldMiddleware CreateExecutor(); + + /// + /// Configures the field where the filters are applied. This can be used to add context + /// data to the field. + /// + /// + /// the field descriptor where the filtering is applied + /// + void ConfigureField(IObjectFieldDescriptor fieldDescriptor); } } diff --git a/src/HotChocolate/Data/src/Data/Filters/Expressions/Extensions/QueryableFilterContextExtensions.cs b/src/HotChocolate/Data/src/Data/Filters/Expressions/Extensions/QueryableFilterContextExtensions.cs index c9625d68921..a12e3d8d07a 100644 --- a/src/HotChocolate/Data/src/Data/Filters/Expressions/Extensions/QueryableFilterContextExtensions.cs +++ b/src/HotChocolate/Data/src/Data/Filters/Expressions/Extensions/QueryableFilterContextExtensions.cs @@ -7,23 +7,6 @@ namespace HotChocolate.Data.Filters.Expressions { public static class QueryableFilterVisitorContextExtensions { - public static FilterScope AddIsNullClosure( - this QueryableFilterContext context) - { - var closure = new QueryableScope( - context.RuntimeTypes.Peek(), - "_s" + context.Scopes.Count, - false); - - context.Scopes.Push(closure); - - context.GetLevel() - .Enqueue( - FilterExpressionBuilder.Equals(context.GetClosure().Parameter, null)); - - return closure; - } - public static QueryableScope GetClosure( this QueryableFilterContext context) => (QueryableScope)context.GetScope(); diff --git a/src/HotChocolate/Data/src/Data/Filters/Expressions/QueryableFilterProvider.cs b/src/HotChocolate/Data/src/Data/Filters/Expressions/QueryableFilterProvider.cs index 937e44a8a7c..e1e507cc775 100644 --- a/src/HotChocolate/Data/src/Data/Filters/Expressions/QueryableFilterProvider.cs +++ b/src/HotChocolate/Data/src/Data/Filters/Expressions/QueryableFilterProvider.cs @@ -9,10 +9,18 @@ namespace HotChocolate.Data.Filters.Expressions { + public delegate QueryableFilterContext VisitFilterArgument( + IValueNode filterValueNode, + IFilterInputType filterInputType, + bool inMemory); + public class QueryableFilterProvider : FilterProvider { - public static readonly string ContextDataKeys = nameof(QueryableFilterProvider); + public static readonly string ContextArgumentNameKey = "FilterArgumentName"; + public static readonly string ContextVisitFilterArgumentKey = nameof(VisitFilterArgument); + public static readonly string SkipFilteringKey = "SkipFiltering"; + public static readonly string ContextValueNodeKey = nameof(QueryableFilterProvider); public QueryableFilterProvider() { @@ -41,13 +49,18 @@ public override FieldMiddleware CreateExecutor(NameString argumentN // next we get the filter argument. If the filter argument is already on the context // we use this. This enabled overriding the context with LocalContextData IInputField argument = context.Field.Arguments[argumentName]; - IValueNode filter = context.LocalContextData.ContainsKey(ContextDataKeys) && - context.LocalContextData[ContextDataKeys] is IValueNode node + IValueNode filter = context.LocalContextData.ContainsKey(ContextValueNodeKey) && + context.LocalContextData[ContextValueNodeKey] is IValueNode node ? node : context.ArgumentLiteral(argumentName); // if no filter is defined we can stop here and yield back control. - if (filter.IsNull()) + if (filter.IsNull() || + (context.LocalContextData.TryGetValue( + SkipFilteringKey, + out object? skipObject) && + skipObject is bool skip && + skip)) { return; } @@ -63,15 +76,18 @@ public override FieldMiddleware CreateExecutor(NameString argumentN source = e.AsQueryable(); } - if (source != null && argument.Type is IFilterInputType filterInput) + if (source != null && + argument.Type is IFilterInputType filterInput && + context.Field.ContextData.TryGetValue( + ContextVisitFilterArgumentKey, + out object? executorObj) && + executorObj is VisitFilterArgument executor) { - var visitorContext = new QueryableFilterContext( + QueryableFilterContext visitorContext = executor( + filter, filterInput, source is EnumerableQuery); - // rewrite GraphQL input object into expression tree. - Visitor.Visit(filter, visitorContext); - // compile expression tree if (visitorContext.TryCreateLambda( out Expression>? where)) @@ -92,5 +108,33 @@ public override FieldMiddleware CreateExecutor(NameString argumentN } } } + + public override void ConfigureField( + NameString argumentName, + IObjectFieldDescriptor descriptor) + { + QueryableFilterContext VisitFilterArgumentExecutor( + IValueNode valueNode, + IFilterInputType filterInput, + bool inMemory) + { + var visitorContext = new QueryableFilterContext( + filterInput, + inMemory); + + // rewrite GraphQL input object into expression tree. + Visitor.Visit(valueNode, visitorContext); + + return visitorContext; + } + + descriptor.ConfigureContextData( + contextData => + { + contextData[ContextVisitFilterArgumentKey] = + (VisitFilterArgument)VisitFilterArgumentExecutor; + contextData[ContextArgumentNameKey] = argumentName; + }); + } } } diff --git a/src/HotChocolate/Data/src/Data/Filters/Extensions/FilterFieldDescriptorExtensions.cs b/src/HotChocolate/Data/src/Data/Filters/Extensions/FilterFieldDescriptorExtensions.cs index 101a93d2221..d46d6f941ae 100644 --- a/src/HotChocolate/Data/src/Data/Filters/Extensions/FilterFieldDescriptorExtensions.cs +++ b/src/HotChocolate/Data/src/Data/Filters/Extensions/FilterFieldDescriptorExtensions.cs @@ -8,12 +8,14 @@ namespace HotChocolate.Data.Filters public static class FilterFieldDescriptorExtensions { public static void MakeNullable(this IFilterFieldDescriptor descriptor) => - descriptor.Extend().OnBeforeCreate( - (c, def) => def.Type = RewriteTypeToNullableType(def, c.TypeInspector)); + descriptor.Extend() + .OnBeforeCreate( + (c, def) => def.Type = RewriteTypeToNullableType(def, c.TypeInspector)); public static void MakeNullable(this IFilterOperationFieldDescriptor descriptor) => - descriptor.Extend().OnBeforeCreate( - (c, def) => def.Type = RewriteTypeToNullableType(def, c.TypeInspector)); + descriptor.Extend() + .OnBeforeCreate( + (c, def) => def.Type = RewriteTypeToNullableType(def, c.TypeInspector)); private static ITypeReference RewriteTypeToNullableType( FilterFieldDefinition definition, @@ -25,7 +27,8 @@ public static class FilterFieldDescriptorExtensions { return extendedTypeRef.Type.IsNullable ? extendedTypeRef - : extendedTypeRef.WithType(typeInspector.ChangeNullability(extendedTypeRef.Type, true)); + : extendedTypeRef.WithType( + typeInspector.ChangeNullability(extendedTypeRef.Type, true)); } if (reference is SchemaTypeReference schemaRef) diff --git a/src/HotChocolate/Data/src/Data/Filters/Extensions/FilterObjectFieldDescriptorExtensions.cs b/src/HotChocolate/Data/src/Data/Filters/Extensions/FilterObjectFieldDescriptorExtensions.cs index 242dd096134..4ceb0ceee55 100644 --- a/src/HotChocolate/Data/src/Data/Filters/Extensions/FilterObjectFieldDescriptorExtensions.cs +++ b/src/HotChocolate/Data/src/Data/Filters/Extensions/FilterObjectFieldDescriptorExtensions.cs @@ -220,6 +220,9 @@ public static class FilterObjectFieldDescriptorExtensions IFilterInputType type = context.GetType(argumentTypeReference); IFilterConvention convention = context.DescriptorContext.GetFilterConvention(scope); + var fieldDescriptor = ObjectFieldDescriptor.From(context.DescriptorContext, definition); + convention.ConfigureField(fieldDescriptor); + MethodInfo factory = _factoryTemplate.MakeGenericMethod(type.EntityType.Source); var middleware = (FieldMiddleware)factory.Invoke(null, new object[] { convention })!; var index = definition.MiddlewareComponents.IndexOf(placeholder); diff --git a/src/HotChocolate/Data/src/Data/Filters/Visitor/FilterProvider.cs b/src/HotChocolate/Data/src/Data/Filters/Visitor/FilterProvider.cs index af968c30787..9fd3debaff6 100644 --- a/src/HotChocolate/Data/src/Data/Filters/Visitor/FilterProvider.cs +++ b/src/HotChocolate/Data/src/Data/Filters/Visitor/FilterProvider.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using HotChocolate.Resolvers; +using HotChocolate.Types; using HotChocolate.Types.Descriptors; using HotChocolate.Utilities; using static HotChocolate.Data.DataResources; @@ -17,6 +18,7 @@ public abstract class FilterProvider { private readonly List> _fieldHandlers = new List>(); + private Action>? _configure; protected FilterProvider() @@ -75,14 +77,14 @@ protected override FilterProviderDefinition CreateDefinition(IConventionContext case null when services.TryGetOrCreateService( handler.Type, out IFilterFieldHandler? service): - _fieldHandlers .Add(service); + _fieldHandlers.Add(service); break; case null: context.ReportError( FilterProvider_UnableToCreateFieldHandler(this, handler.Type)); break; case IFilterFieldHandler casted: - _fieldHandlers .Add(casted); + _fieldHandlers.Add(casted); break; } } @@ -91,5 +93,11 @@ protected override FilterProviderDefinition CreateDefinition(IConventionContext protected virtual void Configure(IFilterProviderDescriptor descriptor) { } public abstract FieldMiddleware CreateExecutor(NameString argumentName); + + public virtual void ConfigureField( + NameString argumentName, + IObjectFieldDescriptor descriptor) + { + } } } diff --git a/src/HotChocolate/Data/src/Data/Filters/Visitor/IFilterFieldHandler.cs b/src/HotChocolate/Data/src/Data/Filters/Visitor/IFilterFieldHandler.cs index 510a8571c62..bd21bfc8a9a 100644 --- a/src/HotChocolate/Data/src/Data/Filters/Visitor/IFilterFieldHandler.cs +++ b/src/HotChocolate/Data/src/Data/Filters/Visitor/IFilterFieldHandler.cs @@ -4,6 +4,14 @@ namespace HotChocolate.Data.Filters { public interface IFilterFieldHandler { + /// + /// Tests if this field handler can handle a field If it can handle the field it + /// will be attached to the + /// + /// The discovery context of the schema + /// The definition of the declaring type of the field + /// The definition of the field + /// Returns true if the field can be handled bool CanHandle( ITypeDiscoveryContext context, IFilterInputTypeDefinition typeDefinition, diff --git a/src/HotChocolate/Data/src/Data/Filters/Visitor/IFilterFieldHandler~1.cs b/src/HotChocolate/Data/src/Data/Filters/Visitor/IFilterFieldHandler~1.cs index 96a3c6f59fa..75b0e9cc0d9 100644 --- a/src/HotChocolate/Data/src/Data/Filters/Visitor/IFilterFieldHandler~1.cs +++ b/src/HotChocolate/Data/src/Data/Filters/Visitor/IFilterFieldHandler~1.cs @@ -8,12 +8,36 @@ public interface IFilterFieldHandler : IFilterFieldHandler where TContext : IFilterVisitorContext { + /// + /// This method is called when the encounters a + /// field + /// + /// The of the visitor + /// The field that is currently being visited + /// The value node of this field + /// + /// The that the visitor should + /// continue with + /// + /// If true is returned the action is used for further processing bool TryHandleEnter( TContext context, IFilterField field, ObjectFieldNode node, [NotNullWhen(true)] out ISyntaxVisitorAction? action); + /// + /// This method is called when the leaves a + /// field + /// + /// The of the visitor + /// The field that is currently being visited + /// The value node of this field + /// + /// The that the visitor should + /// continue with + /// + /// If true is returned the action is used for further processing bool TryHandleLeave( TContext context, IFilterField field, diff --git a/src/HotChocolate/Data/src/Data/Filters/Visitor/IFilterProvider.cs b/src/HotChocolate/Data/src/Data/Filters/Visitor/IFilterProvider.cs index 4889c5c1255..caf53673966 100644 --- a/src/HotChocolate/Data/src/Data/Filters/Visitor/IFilterProvider.cs +++ b/src/HotChocolate/Data/src/Data/Filters/Visitor/IFilterProvider.cs @@ -1,13 +1,33 @@ using System.Collections.Generic; using HotChocolate.Resolvers; +using HotChocolate.Types; namespace HotChocolate.Data.Filters { public interface IFilterProvider { + /// + /// A collection of all that this provider knows. + /// IReadOnlyCollection FieldHandlers { get; } + /// + /// Creates a middleware that represents the filter execution logic + /// for the specified entity type. + /// + /// + /// The entity type for which an filter executor shall be created. + /// + /// + /// Returns a field middleware which represents the filter execution logic + /// for the specified entity type. + /// FieldMiddleware CreateExecutor(NameString argumentName); + + /// + /// Configures the field where the filters are applied. This can be used to add context + /// data to the field. + /// + void ConfigureField(NameString argumentName, IObjectFieldDescriptor descriptor); } } - diff --git a/src/HotChocolate/Data/src/Data/Filters/Visitor/IFilterProviderDescriptor.cs b/src/HotChocolate/Data/src/Data/Filters/Visitor/IFilterProviderDescriptor.cs index bf1681c738e..9ac61eaec10 100644 --- a/src/HotChocolate/Data/src/Data/Filters/Visitor/IFilterProviderDescriptor.cs +++ b/src/HotChocolate/Data/src/Data/Filters/Visitor/IFilterProviderDescriptor.cs @@ -3,9 +3,23 @@ public interface IFilterProviderDescriptor : IFluent where TContext : IFilterVisitorContext { + /// + /// Adds a to the provider + /// This field handler is either injected by the dependency injection or created by an + /// activator + /// + /// The type of the field handler + /// The descriptor that this methods was called on IFilterProviderDescriptor AddFieldHandler() where TFieldHandler : IFilterFieldHandler; + /// + /// Adds an instance of a to the provider + /// This instance is directly used by the visitor for executing filters + /// + /// + /// The type of the field handler + /// The descriptor that this methods was called on IFilterProviderDescriptor AddFieldHandler( TFieldHandler fieldHandler) where TFieldHandler : IFilterFieldHandler; diff --git a/src/HotChocolate/Data/src/Data/Filters/Visitor/IFilterVisitorContext~1.cs b/src/HotChocolate/Data/src/Data/Filters/Visitor/IFilterVisitorContext~1.cs index 772a45bf44e..6444a87ae37 100644 --- a/src/HotChocolate/Data/src/Data/Filters/Visitor/IFilterVisitorContext~1.cs +++ b/src/HotChocolate/Data/src/Data/Filters/Visitor/IFilterVisitorContext~1.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using HotChocolate.Utilities; namespace HotChocolate.Data.Filters { @@ -10,4 +9,4 @@ public interface IFilterVisitorContext FilterScope CreateScope(); } -} \ No newline at end of file +} diff --git a/src/HotChocolate/Data/src/Data/HotChocolate.Data.csproj b/src/HotChocolate/Data/src/Data/HotChocolate.Data.csproj index 7fe40af2cb9..c71f788880f 100644 --- a/src/HotChocolate/Data/src/Data/HotChocolate.Data.csproj +++ b/src/HotChocolate/Data/src/Data/HotChocolate.Data.csproj @@ -9,6 +9,7 @@ + diff --git a/src/HotChocolate/Data/src/Data/Projections/Attributes/IsProjectedAttribute.cs b/src/HotChocolate/Data/src/Data/Projections/Attributes/IsProjectedAttribute.cs new file mode 100644 index 00000000000..ee3ce6d60e1 --- /dev/null +++ b/src/HotChocolate/Data/src/Data/Projections/Attributes/IsProjectedAttribute.cs @@ -0,0 +1,33 @@ +using System; +using System.Reflection; +using HotChocolate.Types; +using HotChocolate.Types.Descriptors; + +namespace HotChocolate.Data +{ + [AttributeUsage( + AttributeTargets.Property | AttributeTargets.Method, + Inherited = true, + AllowMultiple = true)] + public class IsProjectedAttribute : ObjectFieldDescriptorAttribute + { + private readonly bool _isProjected = true; + + public IsProjectedAttribute(bool isProjected) + { + _isProjected = isProjected; + } + + public IsProjectedAttribute() + { + } + + public override void OnConfigure( + IDescriptorContext context, + IObjectFieldDescriptor descriptor, + MemberInfo member) + { + descriptor.IsProjected(_isProjected); + } + } +} diff --git a/src/HotChocolate/Data/src/Data/Projections/Attributes/UseFirstOrDefaultAttribute.cs b/src/HotChocolate/Data/src/Data/Projections/Attributes/UseFirstOrDefaultAttribute.cs new file mode 100644 index 00000000000..b1dd8c51080 --- /dev/null +++ b/src/HotChocolate/Data/src/Data/Projections/Attributes/UseFirstOrDefaultAttribute.cs @@ -0,0 +1,18 @@ +using System.Reflection; +using HotChocolate.Types; +using HotChocolate.Types.Descriptors; + +namespace HotChocolate.Data +{ + public sealed class UseFirstOrDefaultAttribute + : ObjectFieldDescriptorAttribute + { + public override void OnConfigure( + IDescriptorContext context, + IObjectFieldDescriptor descriptor, + MemberInfo member) + { + descriptor.UseFirstOrDefault(); + } + } +} diff --git a/src/HotChocolate/Data/src/Data/Projections/Attributes/UseProjectionAttribute.cs b/src/HotChocolate/Data/src/Data/Projections/Attributes/UseProjectionAttribute.cs new file mode 100644 index 00000000000..37aabeb69ee --- /dev/null +++ b/src/HotChocolate/Data/src/Data/Projections/Attributes/UseProjectionAttribute.cs @@ -0,0 +1,18 @@ +using System.Reflection; +using HotChocolate.Types; +using HotChocolate.Types.Descriptors; + +namespace HotChocolate.Data +{ + public sealed class UseProjectionAttribute + : ObjectFieldDescriptorAttribute + { + public override void OnConfigure( + IDescriptorContext context, + IObjectFieldDescriptor descriptor, + MemberInfo member) + { + descriptor.UseProjection(); + } + } +} diff --git a/src/HotChocolate/Data/src/Data/Projections/Attributes/UseSingleOrDefaultAttribute.cs b/src/HotChocolate/Data/src/Data/Projections/Attributes/UseSingleOrDefaultAttribute.cs new file mode 100644 index 00000000000..515ecc4f23c --- /dev/null +++ b/src/HotChocolate/Data/src/Data/Projections/Attributes/UseSingleOrDefaultAttribute.cs @@ -0,0 +1,18 @@ +using System.Reflection; +using HotChocolate.Types; +using HotChocolate.Types.Descriptors; + +namespace HotChocolate.Data +{ + public sealed class UseSingleOrDefaultAttribute + : ObjectFieldDescriptorAttribute + { + public override void OnConfigure( + IDescriptorContext context, + IObjectFieldDescriptor descriptor, + MemberInfo member) + { + descriptor.UseSingleOrDefault(); + } + } +} diff --git a/src/HotChocolate/Data/src/Data/Projections/Convention/Extensions/ProjectionConventionDescriptorExtensions.cs b/src/HotChocolate/Data/src/Data/Projections/Convention/Extensions/ProjectionConventionDescriptorExtensions.cs new file mode 100644 index 00000000000..498aa36272e --- /dev/null +++ b/src/HotChocolate/Data/src/Data/Projections/Convention/Extensions/ProjectionConventionDescriptorExtensions.cs @@ -0,0 +1,11 @@ +using HotChocolate.Data.Projections.Expressions; + +namespace HotChocolate.Data.Projections +{ + public static class ProjectionConventionDescriptorExtensions + { + public static IProjectionConventionDescriptor AddDefaults( + this IProjectionConventionDescriptor descriptor) => + descriptor.Provider(new QueryableProjectionProvider(x => x.AddDefaults())); + } +} diff --git a/src/HotChocolate/Data/src/Data/Projections/Convention/Extensions/ProjectionProviderDescriptorExtensions.cs b/src/HotChocolate/Data/src/Data/Projections/Convention/Extensions/ProjectionProviderDescriptorExtensions.cs new file mode 100644 index 00000000000..d5724e3d5cc --- /dev/null +++ b/src/HotChocolate/Data/src/Data/Projections/Convention/Extensions/ProjectionProviderDescriptorExtensions.cs @@ -0,0 +1,38 @@ +using System; +using HotChocolate.Data.Projections.Expressions.Handlers; +using HotChocolate.Data.Projections.Handlers; + +namespace HotChocolate.Data.Projections +{ + public static class ProjectionProviderDescriptorExtensions + { + public static IProjectionProviderDescriptor AddDefaults( + this IProjectionProviderDescriptor descriptor) => + descriptor.RegisterQueryableHandler(); + + public static IProjectionProviderDescriptor RegisterQueryableHandler( + this IProjectionProviderDescriptor descriptor) + { + if (descriptor is null) + { + throw new ArgumentNullException(nameof(descriptor)); + } + + descriptor.RegisterFieldHandler(); + descriptor.RegisterFieldHandler(); + descriptor.RegisterFieldHandler(); + + descriptor.RegisterFieldInterceptor(); + descriptor.RegisterFieldInterceptor(); + descriptor.RegisterFieldInterceptor(); + descriptor.RegisterFieldInterceptor(); + + descriptor.RegisterOptimizer(); + descriptor.RegisterOptimizer(); + descriptor.RegisterOptimizer(); + descriptor.RegisterOptimizer(); + + return descriptor; + } + } +} diff --git a/src/HotChocolate/Data/src/Data/Projections/Convention/IProjectionConvention.cs b/src/HotChocolate/Data/src/Data/Projections/Convention/IProjectionConvention.cs new file mode 100644 index 00000000000..f745aeb33d2 --- /dev/null +++ b/src/HotChocolate/Data/src/Data/Projections/Convention/IProjectionConvention.cs @@ -0,0 +1,37 @@ +using HotChocolate.Execution.Processing; +using HotChocolate.Resolvers; +using HotChocolate.Types.Descriptors; + +namespace HotChocolate.Data.Projections +{ + /// + /// The projection convention provides defaults for projections projections. + /// + public interface IProjectionConvention : IConvention + { + /// + /// Creates a middleware that represents the projection execution logic + /// for the specified entity type. + /// + /// + /// The entity type for which an projection executor shall be created. + /// + /// + /// Returns a field middleware which represents the projection execution logic + /// for the specified entity type. + /// + FieldMiddleware CreateExecutor(); + + /// + /// Rewrites a selection optimized for projection + /// + /// The context of the optimizer + /// The selection to rewrite + /// + /// Either a new rewritten selection or the same one if no rewriting was performed + /// + Selection RewriteSelection( + SelectionOptimizerContext context, + Selection selection); + } +} diff --git a/src/HotChocolate/Data/src/Data/Projections/Convention/IProjectionConventionDescriptor.cs b/src/HotChocolate/Data/src/Data/Projections/Convention/IProjectionConventionDescriptor.cs new file mode 100644 index 00000000000..503c62d119a --- /dev/null +++ b/src/HotChocolate/Data/src/Data/Projections/Convention/IProjectionConventionDescriptor.cs @@ -0,0 +1,28 @@ +using System; + +namespace HotChocolate.Data.Projections +{ + public interface IProjectionConventionDescriptor + { + /// + /// Specifies the projection provider. + /// + /// The projection provider type. + IProjectionConventionDescriptor Provider() + where TProvider : class, IProjectionProvider; + + /// + /// Specifies the projection provider. + /// + /// The concrete projection provider that shall be used. + /// The projection provider type. + IProjectionConventionDescriptor Provider(TProvider provider) + where TProvider : class, IProjectionProvider; + + /// + /// Specifies the projection provider. + /// + /// The projection provider type. + IProjectionConventionDescriptor Provider(Type provider); + } +} diff --git a/src/HotChocolate/Data/src/Data/Projections/Convention/IProjectionFieldInterceptor.cs b/src/HotChocolate/Data/src/Data/Projections/Convention/IProjectionFieldInterceptor.cs new file mode 100644 index 00000000000..806d9deabc1 --- /dev/null +++ b/src/HotChocolate/Data/src/Data/Projections/Convention/IProjectionFieldInterceptor.cs @@ -0,0 +1,16 @@ +using HotChocolate.Execution.Processing; + +namespace HotChocolate.Data.Projections +{ + public interface IProjectionFieldInterceptor + { + /// + /// Tests if this interceptor can handle a selection If it can handle the selection it + /// will be attached to the compiled selection set on the + /// type + /// + /// The selection to test for + /// Returns true if the selection can be handled + bool CanHandle(ISelection selection); + } +} diff --git a/src/HotChocolate/Data/src/Data/Projections/Convention/IProjectionFieldInterceptor`1.cs b/src/HotChocolate/Data/src/Data/Projections/Convention/IProjectionFieldInterceptor`1.cs new file mode 100644 index 00000000000..16f202368a8 --- /dev/null +++ b/src/HotChocolate/Data/src/Data/Projections/Convention/IProjectionFieldInterceptor`1.cs @@ -0,0 +1,29 @@ +using HotChocolate.Execution.Processing; + +namespace HotChocolate.Data.Projections +{ + public interface IProjectionFieldInterceptor + : IProjectionFieldInterceptor + where TContext : IProjectionVisitorContext + { + /// + /// This method is called before the enter and leave methods of a + /// + /// + /// The context of the + /// The current selection + void BeforeProjection( + TContext context, + ISelection selection); + + /// + /// This method is called after the enter and leave methods of a + /// + /// + /// The context of the + /// The current selection + void AfterProjection( + TContext context, + ISelection selection); + } +} diff --git a/src/HotChocolate/Data/src/Data/Projections/Convention/IProjectionProvider.cs b/src/HotChocolate/Data/src/Data/Projections/Convention/IProjectionProvider.cs new file mode 100644 index 00000000000..73c913e539d --- /dev/null +++ b/src/HotChocolate/Data/src/Data/Projections/Convention/IProjectionProvider.cs @@ -0,0 +1,37 @@ +using HotChocolate.Execution.Processing; +using HotChocolate.Resolvers; +using HotChocolate.Types.Descriptors; + +namespace HotChocolate.Data.Projections +{ + /// + /// The projection convention provides defaults for rewriter and providers filters. + /// + public interface IProjectionProvider : IConvention + { + /// + /// Creates a middleware that represents the filter execution logic + /// for the specified entity type. + /// + /// + /// The entity type for which an filter executor shall be created. + /// + /// + /// Returns a field middleware which represents the filter execution logic + /// for the specified entity type. + /// + FieldMiddleware CreateExecutor(); + + /// + /// Rewrites a selection optimized for projection + /// + /// The context of the optimizer + /// The selection to rewrite + /// + /// Either a new rewritten selection or the same one if no rewriting was performed + /// + Selection RewriteSelection( + SelectionOptimizerContext context, + Selection selection); + } +} diff --git a/src/HotChocolate/Data/src/Data/Projections/Convention/IProjectionProviderConvention.cs b/src/HotChocolate/Data/src/Data/Projections/Convention/IProjectionProviderConvention.cs new file mode 100644 index 00000000000..20e71fb5eab --- /dev/null +++ b/src/HotChocolate/Data/src/Data/Projections/Convention/IProjectionProviderConvention.cs @@ -0,0 +1,9 @@ +using HotChocolate.Types.Descriptors; + +namespace HotChocolate.Data.Projections +{ + internal interface IProjectionProviderConvention + { + void Initialize(IConventionContext context); + } +} diff --git a/src/HotChocolate/Data/src/Data/Projections/Convention/IProjectionProviderDescriptor.cs b/src/HotChocolate/Data/src/Data/Projections/Convention/IProjectionProviderDescriptor.cs new file mode 100644 index 00000000000..b0c4afb3eb3 --- /dev/null +++ b/src/HotChocolate/Data/src/Data/Projections/Convention/IProjectionProviderDescriptor.cs @@ -0,0 +1,70 @@ +namespace HotChocolate.Data.Projections +{ + /// + /// This descriptor is used to configure a . + /// + public interface IProjectionProviderDescriptor + { + /// + /// Registers a field handler that is used to project selections to the database + /// This field handler is either injected by the dependency injection or created by an + /// activator + /// + /// The type of the field handler + /// The descriptor that this methods was called on + IProjectionProviderDescriptor RegisterFieldHandler() + where THandler : IProjectionFieldHandler; + + /// + /// Registers an instance of a field handler that is used to project selections to the + /// database. This instance is directly used by the visitor for projection the selection set + /// + /// The type of the field handler + /// The descriptor that this methods was called on + IProjectionProviderDescriptor RegisterFieldHandler(THandler handler) + where THandler : IProjectionFieldHandler; + + /// + /// Registers a field interceptor that is used to intercept the projection of a field + /// This can be used to emulate middlewares like UseFiltering and modify the context + /// before the actual field projection happens + /// + /// The type of the field interceptor + /// The descriptor that this methods was called on + IProjectionProviderDescriptor RegisterFieldInterceptor() + where THandler : IProjectionFieldInterceptor; + + /// + /// Registers an instance of a field interceptor that is used to intercept the projection + /// of a field. This can be used to emulate middlewares like UseFiltering and modify + /// the context before the actual field projection happens. + /// This instance is directly used by the visitor for projection the selection set + /// + /// The type of the field interceptor + /// The descriptor that this methods was called on + IProjectionProviderDescriptor RegisterFieldInterceptor(THandler handler) + where THandler : IProjectionFieldInterceptor; + + /// + /// Registers a field optimizer that is used to optimize a selection set before it + /// is projected. With optimizers you can delete, add or rewrite fields on the selection + /// set. This can also be used to rewrite the resolver pipeline. + /// + /// The type of the field optimizer + /// The descriptor that this methods was called on + IProjectionProviderDescriptor RegisterOptimizer() + where THandler : IProjectionOptimizer; + + /// + /// Registers a instance of an optimizer that is used to optimize a selection set before it + /// is projected. With optimizers you can delete, add or rewrite fields on the selection + /// set. This can also be used to rewrite the resolver pipeline. + /// This instance is directly used by the visitor for projection the selection set + /// + /// + /// + /// + IProjectionProviderDescriptor RegisterOptimizer(THandler handler) + where THandler : IProjectionOptimizer; + } +} diff --git a/src/HotChocolate/Data/src/Data/Projections/Convention/ProjectionConvention.cs b/src/HotChocolate/Data/src/Data/Projections/Convention/ProjectionConvention.cs new file mode 100644 index 00000000000..93e453ac71e --- /dev/null +++ b/src/HotChocolate/Data/src/Data/Projections/Convention/ProjectionConvention.cs @@ -0,0 +1,88 @@ +using System; +using HotChocolate.Execution.Processing; +using HotChocolate.Resolvers; +using HotChocolate.Types.Descriptors; +using HotChocolate.Utilities; +using static HotChocolate.Data.ThrowHelper; + +namespace HotChocolate.Data.Projections +{ + public class ProjectionConvention + : Convention + , IProjectionConvention + { + private Action? _configure; + private IProjectionProvider _provider; + + public const string IsProjectedKey = nameof(IsProjectedKey); + public const string AlwaysProjectedFieldsKey = nameof(AlwaysProjectedFieldsKey); + + protected ProjectionConvention() + { + _configure = Configure; + } + + public ProjectionConvention(Action configure) + { + _configure = configure ?? + throw new ArgumentNullException(nameof(configure)); + } + + protected override ProjectionConventionDefinition CreateDefinition( + IConventionContext context) + { + if (_configure is null) + { + throw new InvalidOperationException( + DataResources.ProjectionConvention_NoConfigurationSpecified); + } + + var descriptor = ProjectionConventionDescriptor.New( + context.DescriptorContext, + context.Scope); + + _configure(descriptor); + _configure = null; + + return descriptor.CreateDefinition(); + } + + protected virtual void Configure(IProjectionConventionDescriptor descriptor) + { + } + + protected override void OnComplete( + IConventionContext context, + ProjectionConventionDefinition definition) + { + if (definition.Provider is null) + { + throw ProjectionConvention_NoProviderFound(GetType(), definition.Scope); + } + + if (definition.ProviderInstance is null) + { + _provider = + context.Services.GetOrCreateService(definition.Provider) ?? + throw ProjectionConvention_NoProviderFound(GetType(), definition.Scope); + } + else + { + _provider = definition.ProviderInstance; + } + + if (_provider is IProjectionProviderConvention init) + { + init.Initialize(context); + } + } + + public FieldMiddleware CreateExecutor() => + _provider.CreateExecutor(); + + public Selection RewriteSelection( + SelectionOptimizerContext context, + Selection selection) => + _provider.RewriteSelection(context, selection); + } +} diff --git a/src/HotChocolate/Data/src/Data/Projections/Convention/ProjectionConventionDefinition.cs b/src/HotChocolate/Data/src/Data/Projections/Convention/ProjectionConventionDefinition.cs new file mode 100644 index 00000000000..d7cc644af08 --- /dev/null +++ b/src/HotChocolate/Data/src/Data/Projections/Convention/ProjectionConventionDefinition.cs @@ -0,0 +1,14 @@ +using System; +using HotChocolate.Types; + +namespace HotChocolate.Data.Projections +{ + public class ProjectionConventionDefinition : IHasScope + { + public string? Scope { get; set; } + + public Type? Provider { get; set; } + + public IProjectionProvider? ProviderInstance { get; set; } + } +} diff --git a/src/HotChocolate/Data/src/Data/Projections/Convention/ProjectionConventionDescriptor.cs b/src/HotChocolate/Data/src/Data/Projections/Convention/ProjectionConventionDescriptor.cs new file mode 100644 index 00000000000..eccb42f8ef8 --- /dev/null +++ b/src/HotChocolate/Data/src/Data/Projections/Convention/ProjectionConventionDescriptor.cs @@ -0,0 +1,70 @@ +using System; +using HotChocolate.Types.Descriptors; + +namespace HotChocolate.Data.Projections +{ + public class ProjectionConventionDescriptor + : IProjectionConventionDescriptor + { + protected ProjectionConventionDescriptor( + IDescriptorContext context, + string? scope) + { + Context = context ?? throw new ArgumentNullException(nameof(context)); + Definition.Scope = scope; + } + + protected IDescriptorContext Context { get; } + + protected ProjectionConventionDefinition Definition { get; } = + new ProjectionConventionDefinition(); + + public ProjectionConventionDefinition CreateDefinition() + { + return Definition; + } + + /// + public IProjectionConventionDescriptor Provider() + where TProvider : class, IProjectionProvider => + Provider(typeof(TProvider)); + + /// + public IProjectionConventionDescriptor Provider(TProvider provider) + where TProvider : class, IProjectionProvider + { + Definition.Provider = typeof(TProvider); + Definition.ProviderInstance = provider; + return this; + } + + /// + public IProjectionConventionDescriptor Provider(Type provider) + { + if (provider is null) + { + throw new ArgumentNullException(nameof(provider)); + } + + if (!typeof(IProjectionProvider).IsAssignableFrom(provider)) + { + throw new ArgumentException( + DataResources.ProjectionConventionDescriptor_MustImplementIProjectionProvider, + nameof(provider)); + } + + Definition.Provider = provider; + return this; + } + + /// + /// Creates a new descriptor for + /// + /// The descriptor context. + /// The scope + public static ProjectionConventionDescriptor New( + IDescriptorContext context, + string? scope) => + new ProjectionConventionDescriptor(context, scope); + } +} diff --git a/src/HotChocolate/Data/src/Data/Projections/Convention/ProjectionProvider.cs b/src/HotChocolate/Data/src/Data/Projections/Convention/ProjectionProvider.cs new file mode 100644 index 00000000000..c5997e8182b --- /dev/null +++ b/src/HotChocolate/Data/src/Data/Projections/Convention/ProjectionProvider.cs @@ -0,0 +1,183 @@ +using System; +using System.Collections.Generic; +using HotChocolate.Execution.Processing; +using HotChocolate.Resolvers; +using HotChocolate.Types.Descriptors; +using HotChocolate.Utilities; +using static HotChocolate.Data.DataResources; +using static HotChocolate.Data.ErrorHelper; +using static HotChocolate.Data.ThrowHelper; + +namespace HotChocolate.Data.Projections +{ + /// + /// The filter convention provides defaults for inferring filters. + /// + public abstract class ProjectionProvider + : Convention + , IProjectionProvider + , IProjectionProviderConvention + { + private Action? _configure; + + private readonly IList _fieldHandlers = + new List(); + + private readonly IList _fieldInterceptors = + new List(); + + private readonly IList _optimizer = new List(); + + public const string ProjectionContextIdentifier = "ProjectionMiddleware"; + + protected ProjectionProvider() + { + _configure = Configure; + } + + public ProjectionProvider(Action configure) + { + _configure = configure ?? + throw new ArgumentNullException(nameof(configure)); + } + + protected override ProjectionProviderDefinition CreateDefinition( + IConventionContext context) + { + if (_configure is null) + { + throw new InvalidOperationException(ProjectionConvention_NoConfigurationSpecified); + } + + var descriptor = ProjectionProviderDescriptor.New( + context.DescriptorContext, + context.Scope); + + _configure(descriptor); + _configure = null; + + return descriptor.CreateDefinition(); + } + + protected virtual void Configure(IProjectionProviderDescriptor descriptor) + { + } + + protected override void OnComplete( + IConventionContext context, + ProjectionProviderDefinition definition) + { + if (definition.Handlers.Count == 0) + { + throw ProjectionProvider_NoHandlersConfigured(this); + } + + IServiceProvider services = new DictionaryServiceProvider( + (typeof(IConventionContext), context), + (typeof(IProjectionProvider), context.Convention), + (typeof(IDescriptorContext), context.DescriptorContext), + (typeof(ITypeInspector), context.DescriptorContext.TypeInspector)) + .Include(context.Services); + + foreach ((Type type, IProjectionFieldHandler? instance) in definition.Handlers) + { + switch (instance) + { + case null when services.TryGetOrCreateService( + type, + out IProjectionFieldHandler? service): + _fieldHandlers.Add(service); + break; + case null: + context.ReportError( + ProjectionConvention_UnableToCreateFieldHandler(this, type)); + break; + default: + _fieldHandlers.Add(instance); + break; + } + } + + foreach ((var type, IProjectionFieldInterceptor? instance) in definition.Interceptors) + { + switch (instance) + { + case null when services.TryGetOrCreateService( + type, + out IProjectionFieldInterceptor? service): + _fieldInterceptors.Add(service); + break; + case null: + context.ReportError( + ProjectionConvention_UnableToCreateFieldHandler(this, type)); + break; + default: + _fieldInterceptors.Add(instance); + break; + } + } + + foreach ((var type, IProjectionOptimizer? instance) in definition.Optimizers) + { + switch (instance) + { + case null when services.TryGetOrCreateService( + type, + out IProjectionOptimizer? service): + _optimizer.Add(service); + break; + case null: + context.ReportError( + ProjectionConvention_UnableToCreateFieldHandler(this, type)); + break; + default: + _optimizer.Add(instance); + break; + } + } + } + + public Selection RewriteSelection( + SelectionOptimizerContext context, + Selection selection) + { + for (var i = 0; i < _optimizer.Count; i++) + { + if (_optimizer[i].CanHandle(selection)) + { + selection = _optimizer[i].RewriteSelection(context, selection); + } + } + + for (var i = 0; i < _fieldHandlers.Count; i++) + { + if (_fieldHandlers[i].CanHandle(selection)) + { + IProjectionFieldHandler fieldHandler = _fieldHandlers[i]; + + for (var m = 0; m < _fieldInterceptors.Count; m++) + { + if (_fieldInterceptors[m].CanHandle(selection)) + { + fieldHandler = fieldHandler.Wrap(_fieldInterceptors[m]); + } + } + + return ProjectionSelection.From( + selection, + fieldHandler); + } + } + + return selection; + } + + public new void Initialize(IConventionContext context) + { + base.Initialize(context); + } + + /// + public abstract FieldMiddleware CreateExecutor(); + } +} diff --git a/src/HotChocolate/Data/src/Data/Projections/Convention/ProjectionProviderDefinition.cs b/src/HotChocolate/Data/src/Data/Projections/Convention/ProjectionProviderDefinition.cs new file mode 100644 index 00000000000..2dd7bb4760a --- /dev/null +++ b/src/HotChocolate/Data/src/Data/Projections/Convention/ProjectionProviderDefinition.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using HotChocolate.Types; + +namespace HotChocolate.Data.Projections +{ + public class ProjectionProviderDefinition : IHasScope + { + public string? Scope { get; set; } + + public IList<(Type, IProjectionFieldHandler?)> Handlers { get; } = + new List<(Type, IProjectionFieldHandler?)>(); + + public IList<(Type, IProjectionFieldInterceptor?)> Interceptors { get; } = + new List<(Type, IProjectionFieldInterceptor?)>(); + + public IList<(Type, IProjectionOptimizer?)> Optimizers { get; } = + new List<(Type, IProjectionOptimizer?)>(); + } +} diff --git a/src/HotChocolate/Data/src/Data/Projections/Convention/ProjectionProviderDescriptor.cs b/src/HotChocolate/Data/src/Data/Projections/Convention/ProjectionProviderDescriptor.cs new file mode 100644 index 00000000000..84f126bd629 --- /dev/null +++ b/src/HotChocolate/Data/src/Data/Projections/Convention/ProjectionProviderDescriptor.cs @@ -0,0 +1,83 @@ +using System; +using HotChocolate.Types.Descriptors; + +namespace HotChocolate.Data.Projections +{ + public class ProjectionProviderDescriptor + : IProjectionProviderDescriptor + { + protected ProjectionProviderDescriptor(IDescriptorContext context, string? scope) + { + Context = context ?? throw new ArgumentNullException(nameof(context)); + Definition.Scope = scope; + } + + protected IDescriptorContext Context { get; } + + protected ProjectionProviderDefinition Definition { get; } = + new ProjectionProviderDefinition(); + + public ProjectionProviderDefinition CreateDefinition() + { + return Definition; + } + + /// + public IProjectionProviderDescriptor RegisterFieldHandler() + where THandler : IProjectionFieldHandler + { + Definition.Handlers.Add((typeof(THandler), null)); + return this; + } + + /// + public IProjectionProviderDescriptor RegisterFieldHandler(THandler handler) + where THandler : IProjectionFieldHandler + { + Definition.Handlers.Add((typeof(THandler), handler)); + return this; + } + + /// + public IProjectionProviderDescriptor RegisterFieldInterceptor() + where THandler : IProjectionFieldInterceptor + { + Definition.Interceptors.Add((typeof(THandler), null)); + return this; + } + + /// + public IProjectionProviderDescriptor RegisterFieldInterceptor(THandler handler) + where THandler : IProjectionFieldInterceptor + { + Definition.Interceptors.Add((typeof(THandler), handler)); + return this; + } + + /// + public IProjectionProviderDescriptor RegisterOptimizer() + where THandler : IProjectionOptimizer + { + Definition.Optimizers.Add((typeof(THandler), null)); + return this; + } + + /// + public IProjectionProviderDescriptor RegisterOptimizer(THandler handler) + where THandler : IProjectionOptimizer + { + Definition.Optimizers.Add((typeof(THandler), handler)); + return this; + } + + /// + /// Creates a new descriptor for + /// + /// The descriptor context. + /// The scope + public static ProjectionProviderDescriptor New( + IDescriptorContext context, + string? scope) => + new ProjectionProviderDescriptor(context, scope); + } +} diff --git a/src/HotChocolate/Data/src/Data/Projections/Convention/ProjectionSelection.cs b/src/HotChocolate/Data/src/Data/Projections/Convention/ProjectionSelection.cs new file mode 100644 index 00000000000..6eefccdac7e --- /dev/null +++ b/src/HotChocolate/Data/src/Data/Projections/Convention/ProjectionSelection.cs @@ -0,0 +1,24 @@ +using HotChocolate.Execution.Processing; + +namespace HotChocolate.Data.Projections +{ + public class ProjectionSelection + : Selection + , IProjectionSelection + { + public ProjectionSelection( + IProjectionFieldHandler handler, + Selection selection) + : base(selection) + { + Handler = handler; + } + + public IProjectionFieldHandler Handler { get; } + + public static ProjectionSelection From( + Selection selection, + IProjectionFieldHandler handler) => + new ProjectionSelection(handler, selection); + } +} diff --git a/src/HotChocolate/Data/src/Data/Projections/Expressions/Handlers/Extensions/ExpressionExtensions.cs b/src/HotChocolate/Data/src/Data/Projections/Expressions/Handlers/Extensions/ExpressionExtensions.cs new file mode 100644 index 00000000000..bea7b4a12d2 --- /dev/null +++ b/src/HotChocolate/Data/src/Data/Projections/Expressions/Handlers/Extensions/ExpressionExtensions.cs @@ -0,0 +1,28 @@ +using System; +using System.Linq.Expressions; +using System.Reflection; + +namespace HotChocolate.Data.Projections.Expressions.Handlers +{ + internal static class ExpressionExtensions + { + public static Expression Append( + this Expression expression, + MemberInfo? memberInfo) => + memberInfo switch + { + PropertyInfo propertyInfo => Expression.Property(expression, propertyInfo), + MethodInfo methodInfo => Expression.Call(expression, methodInfo), + _ => throw new InvalidOperationException() + }; + + public static Type GetReturnType( + this MemberInfo? memberInfo) => + memberInfo switch + { + PropertyInfo propertyInfo => propertyInfo.PropertyType, + MethodInfo methodInfo => methodInfo.ReturnType, + _ => throw new InvalidOperationException() + }; + } +} diff --git a/src/HotChocolate/Data/src/Data/Projections/Expressions/Handlers/Extensions/ProjectionVisitorContextExtensions.cs b/src/HotChocolate/Data/src/Data/Projections/Expressions/Handlers/Extensions/ProjectionVisitorContextExtensions.cs new file mode 100644 index 00000000000..eedf76ba2f9 --- /dev/null +++ b/src/HotChocolate/Data/src/Data/Projections/Expressions/Handlers/Extensions/ProjectionVisitorContextExtensions.cs @@ -0,0 +1,30 @@ +namespace HotChocolate.Data.Projections.Expressions.Handlers +{ + public static class ProjectionVisitorContextExtensions + { + public static void ReportError( + this IProjectionVisitorContext context, + IError error) => + context.Errors.Add(error); + + public static ProjectionScope GetScope( + this IProjectionVisitorContext context) => + context.Scopes.Peek(); + + public static T GetInstance( + this IProjectionVisitorContext context) => + context.Scopes.Peek().Instance.Peek(); + + public static void PushInstance( + this IProjectionVisitorContext context, + T nextExpression) => + context.Scopes.Peek().Instance.Push(nextExpression); + + public static T PopInstance(this IProjectionVisitorContext context) => + context.Scopes.Peek().Instance.Pop(); + + public static ProjectionScope PopScope( + this IProjectionVisitorContext context) => + context.Scopes.Pop(); + } +} diff --git a/src/HotChocolate/Data/src/Data/Projections/Expressions/Handlers/ProjectionFieldHandler.cs b/src/HotChocolate/Data/src/Data/Projections/Expressions/Handlers/ProjectionFieldHandler.cs new file mode 100644 index 00000000000..e9f08481b2b --- /dev/null +++ b/src/HotChocolate/Data/src/Data/Projections/Expressions/Handlers/ProjectionFieldHandler.cs @@ -0,0 +1,46 @@ +using System.Diagnostics.CodeAnalysis; +using HotChocolate.Execution.Processing; + +namespace HotChocolate.Data.Projections.Expressions.Handlers +{ + public abstract class ProjectionFieldHandler + : IProjectionFieldHandler + where T : IProjectionVisitorContext + { + public virtual IProjectionFieldHandler Wrap(IProjectionFieldInterceptor interceptor) + { + if (interceptor is IProjectionFieldInterceptor interceptorOfT) + { + return new ProjectionFieldWrapper(this, interceptorOfT); + } + + return this; + } + + public abstract bool CanHandle(ISelection selection); + + public virtual T OnBeforeEnter(T context, ISelection selection) => context; + + public abstract bool TryHandleEnter( + T context, + ISelection selection, + [NotNullWhen(true)] out ISelectionVisitorAction? action); + + public virtual T OnAfterEnter( + T context, + ISelection selection, + ISelectionVisitorAction action) => context; + + public virtual T OnBeforeLeave(T context, ISelection selection) => context; + + public abstract bool TryHandleLeave( + T context, + ISelection selection, + [NotNullWhen(true)] out ISelectionVisitorAction? action); + + public virtual T OnAfterLeave( + T context, + ISelection selection, + ISelectionVisitorAction action) => context; + } +} diff --git a/src/HotChocolate/Data/src/Data/Projections/Expressions/Handlers/ProjectionFieldWrapper.cs b/src/HotChocolate/Data/src/Data/Projections/Expressions/Handlers/ProjectionFieldWrapper.cs new file mode 100644 index 00000000000..391242bcaac --- /dev/null +++ b/src/HotChocolate/Data/src/Data/Projections/Expressions/Handlers/ProjectionFieldWrapper.cs @@ -0,0 +1,58 @@ +using System.Diagnostics.CodeAnalysis; +using HotChocolate.Execution.Processing; + +namespace HotChocolate.Data.Projections.Expressions.Handlers +{ + public class ProjectionFieldWrapper + : IProjectionFieldHandler + where T : IProjectionVisitorContext + { + private readonly ProjectionFieldHandler _handler; + private readonly IProjectionFieldInterceptor _interceptor; + + public ProjectionFieldWrapper( + ProjectionFieldHandler handler, + IProjectionFieldInterceptor interceptor) + { + _handler = handler; + _interceptor = interceptor; + } + + public bool CanHandle(ISelection selection) => + _handler.CanHandle(selection); + + public IProjectionFieldHandler Wrap(IProjectionFieldInterceptor interceptor) => + _handler.Wrap(interceptor); + + public T OnBeforeEnter(T context, ISelection selection) => + _handler.OnBeforeEnter(context, selection); + + public bool TryHandleEnter( + T context, + ISelection selection, + [NotNullWhen(true)] out ISelectionVisitorAction? action) + { + _interceptor.BeforeProjection(context, selection); + return _handler.TryHandleEnter(context, selection, out action); + } + + public T OnAfterEnter(T context, ISelection selection, ISelectionVisitorAction result) => + _handler.OnAfterEnter(context, selection, result); + + public T OnBeforeLeave(T context, ISelection selection) => + _handler.OnBeforeLeave(context, selection); + + public bool TryHandleLeave( + T context, + ISelection selection, + [NotNullWhen(true)] out ISelectionVisitorAction? action) + { + _handler.TryHandleLeave(context, selection, out action); + _interceptor.AfterProjection(context, selection); + return action is not null; + } + + public T OnAfterLeave(T context, ISelection selection, ISelectionVisitorAction result) => + _handler.OnAfterLeave(context, selection, result); + } +} diff --git a/src/HotChocolate/Data/src/Data/Projections/Expressions/Handlers/QueryableProjectionFieldHandler.cs b/src/HotChocolate/Data/src/Data/Projections/Expressions/Handlers/QueryableProjectionFieldHandler.cs new file mode 100644 index 00000000000..655e6cc0315 --- /dev/null +++ b/src/HotChocolate/Data/src/Data/Projections/Expressions/Handlers/QueryableProjectionFieldHandler.cs @@ -0,0 +1,99 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq.Expressions; +using System.Reflection; +using HotChocolate.Execution.Processing; +using HotChocolate.Types; + +namespace HotChocolate.Data.Projections.Expressions.Handlers +{ + public class QueryableProjectionFieldHandler + : QueryableProjectionHandlerBase + { + public override bool CanHandle(ISelection selection) => + selection.Field.Member is {} && + selection.SelectionSet is not null; + + public override bool TryHandleEnter( + QueryableProjectionContext context, + ISelection selection, + [NotNullWhen(true)] out ISelectionVisitorAction? action) + { + IObjectField field = selection.Field; + + if (field.RuntimeType is null) + { + action = null; + return false; + } + + Expression nestedProperty; + Type memberType; + if (field.Member is PropertyInfo propertyInfo) + { + memberType = propertyInfo.PropertyType; + nestedProperty = Expression.Property(context.GetInstance(), propertyInfo); + } + else if (field.Member is MethodInfo methodInfo) + { + memberType = methodInfo.ReturnType; + nestedProperty = Expression.Call(context.GetInstance(), methodInfo); + } + else + { + throw new InvalidOperationException(); + } + + // We add a new scope for the sub selection. This allows a new member initialization + context.AddScope(memberType); + + // We push the instance onto the new scope. We do not need this instance on the current + // scope. + context.PushInstance(nestedProperty); + + action = SelectionVisitor.Continue; + return true; + } + + public override bool TryHandleLeave( + QueryableProjectionContext context, + ISelection selection, + [NotNullWhen(true)] out ISelectionVisitorAction? action) + { + IObjectField field = selection.Field; + + if (field.RuntimeType is null || + field.Member is null) + { + action = null; + return false; + } + + // Deque last + ProjectionScope scope = context.PopScope(); + + if (!(scope is QueryableProjectionScope queryableScope)) + { + action = null; + return false; + } + + Queue members = queryableScope.Level.Pop(); + MemberInitExpression memberInit = + ProjectionExpressionBuilder.CreateMemberInit(queryableScope.RuntimeType, members); + + if (!context.TryGetQueryableScope(out QueryableProjectionScope? parentScope)) + { + throw new InvalidOperationException(); + } + + parentScope.Level.Peek() + .Enqueue( + Expression.Bind(field.Member, memberInit)); + + action = SelectionVisitor.Continue; + return true; + } + } +} diff --git a/src/HotChocolate/Data/src/Data/Projections/Expressions/Handlers/QueryableProjectionHandlerBase.cs b/src/HotChocolate/Data/src/Data/Projections/Expressions/Handlers/QueryableProjectionHandlerBase.cs new file mode 100644 index 00000000000..a911c8fcf6b --- /dev/null +++ b/src/HotChocolate/Data/src/Data/Projections/Expressions/Handlers/QueryableProjectionHandlerBase.cs @@ -0,0 +1,27 @@ +using System.Diagnostics.CodeAnalysis; +using HotChocolate.Execution.Processing; + +namespace HotChocolate.Data.Projections.Expressions.Handlers +{ + public abstract class QueryableProjectionHandlerBase + : ProjectionFieldHandler + { + public override bool TryHandleEnter( + QueryableProjectionContext context, + ISelection selection, + [NotNullWhen(true)] out ISelectionVisitorAction? action) + { + action = SelectionVisitor.Continue; + return true; + } + + public override bool TryHandleLeave( + QueryableProjectionContext context, + ISelection selection, + [NotNullWhen(true)] out ISelectionVisitorAction? action) + { + action = SelectionVisitor.Continue; + return true; + } + } +} diff --git a/src/HotChocolate/Data/src/Data/Projections/Expressions/Handlers/QueryableProjectionListHandler.cs b/src/HotChocolate/Data/src/Data/Projections/Expressions/Handlers/QueryableProjectionListHandler.cs new file mode 100644 index 00000000000..7d0df37878d --- /dev/null +++ b/src/HotChocolate/Data/src/Data/Projections/Expressions/Handlers/QueryableProjectionListHandler.cs @@ -0,0 +1,100 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Linq.Expressions; +using HotChocolate.Execution.Processing; +using HotChocolate.Types; + +namespace HotChocolate.Data.Projections.Expressions.Handlers +{ + public class QueryableProjectionListHandler + : QueryableProjectionHandlerBase + { + public override bool CanHandle(ISelection selection) => + selection.Field.Member is {} && + selection.Field.Type is ListType || + selection.Field.Type is NonNullType nonNullType && + nonNullType.InnerType() is ListType; + + public override QueryableProjectionContext OnBeforeEnter( + QueryableProjectionContext context, + ISelection selection) + { + IObjectField field = selection.Field; + Expression next = context.GetInstance().Append(field.Member); + + context.PushInstance(next); + + return context; + } + + public override bool TryHandleEnter( + QueryableProjectionContext context, + ISelection selection, + [NotNullWhen(true)] out ISelectionVisitorAction? action) + { + IObjectField field = selection.Field; + + if (field.RuntimeType is null) + { + action = null; + return false; + } + + IOutputType type = field.Type; + + Type clrType = type.IsListType() + ? type.ElementType().ToRuntimeType() + : type.ToRuntimeType(); + + // We add a new scope for the sub selection. This allows a new member initialization + context.AddScope(clrType); + + action = SelectionVisitor.Continue; + return true; + } + + public override bool TryHandleLeave( + QueryableProjectionContext context, + ISelection selection, + [NotNullWhen(true)] out ISelectionVisitorAction? action) + { + IObjectField field = selection.Field; + + if (field.RuntimeType is null || + field.Member is null) + { + action = null; + return false; + } + + ProjectionScope scope = context.PopScope(); + + if (!(scope is QueryableProjectionScope queryableScope) || + !context.TryGetQueryableScope(out QueryableProjectionScope? parentScope)) + { + action = null; + return false; + } + + // in case the projection is empty we do not project. This can happen if the + // field handler below skips fields + if (queryableScope.Level.Count == 0 || queryableScope.Level.Peek().Count == 0) + { + action = SelectionVisitor.Continue; + return true; + } + + Type type = field.Member.GetReturnType(); + + Expression select = queryableScope.CreateSelection( + context.PopInstance(), + type); + + parentScope.Level.Peek().Enqueue( + Expression.Bind(field.Member, select)); + + action = SelectionVisitor.Continue; + return true; + } + } +} diff --git a/src/HotChocolate/Data/src/Data/Projections/Expressions/Handlers/QueryableProjectionScalarHandler.cs b/src/HotChocolate/Data/src/Data/Projections/Expressions/Handlers/QueryableProjectionScalarHandler.cs new file mode 100644 index 00000000000..e04a9cdb707 --- /dev/null +++ b/src/HotChocolate/Data/src/Data/Projections/Expressions/Handlers/QueryableProjectionScalarHandler.cs @@ -0,0 +1,41 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Linq.Expressions; +using System.Reflection; +using HotChocolate.Execution.Processing; +using HotChocolate.Types; + +namespace HotChocolate.Data.Projections.Expressions.Handlers +{ + public class QueryableProjectionScalarHandler + : QueryableProjectionHandlerBase + { + public override bool CanHandle(ISelection selection) => + selection.Field.Member is {} && + selection.SelectionSet is null; + + public override bool TryHandleLeave( + QueryableProjectionContext context, + ISelection selection, + [NotNullWhen(true)] out ISelectionVisitorAction? action) + { + IObjectField field = selection.Field; + + if (context.Scopes.Count > 0 && + context.Scopes.Peek() is QueryableProjectionScope closure && + field.Member is PropertyInfo member) + { + closure.Level.Peek() + .Enqueue( + Expression.Bind( + member, + Expression.Property(closure.Instance.Peek(), member))); + + action = SelectionVisitor.Continue; + return true; + } + + throw new InvalidOperationException(); + } + } +} diff --git a/src/HotChocolate/Data/src/Data/Projections/Expressions/Interceptor/QueryableFilterInterceptor.cs b/src/HotChocolate/Data/src/Data/Projections/Expressions/Interceptor/QueryableFilterInterceptor.cs new file mode 100644 index 00000000000..a340217f8cc --- /dev/null +++ b/src/HotChocolate/Data/src/Data/Projections/Expressions/Interceptor/QueryableFilterInterceptor.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using HotChocolate.Data.Filters; +using HotChocolate.Data.Filters.Expressions; +using HotChocolate.Data.Projections.Expressions; +using HotChocolate.Data.Projections.Expressions.Handlers; +using HotChocolate.Execution.Processing; +using HotChocolate.Types; +using static HotChocolate.Data.ErrorHelper; +using static HotChocolate.Data.Filters.Expressions.QueryableFilterProvider; + +namespace HotChocolate.Data.Projections.Handlers +{ + public class QueryableFilterInterceptor + : IProjectionFieldInterceptor + { + public bool CanHandle(ISelection selection) => + selection.Field.Member is {} && + selection.Field.ContextData.ContainsKey(ContextVisitFilterArgumentKey) && + selection.Field.ContextData.ContainsKey(ContextArgumentNameKey); + + public void BeforeProjection( + QueryableProjectionContext context, + ISelection selection) + { + IObjectField field = selection.Field; + IReadOnlyDictionary contextData = field.ContextData; + + if (contextData.TryGetValue(ContextArgumentNameKey, out object? arg) && + arg is NameString argumentName && + contextData.TryGetValue(ContextVisitFilterArgumentKey, out object? argVisitor) && + argVisitor is VisitFilterArgument argumentVisitor && + context.Selection.Count > 0 && + context.Selection.Peek() + .Arguments.TryCoerceArguments( + context.Context.Variables, + context.Context.ReportError, + out IReadOnlyDictionary? coercedArgs) && + coercedArgs.TryGetValue(argumentName, out var argumentValue) && + argumentValue.Argument.Type is IFilterInputType filterInputType && + argumentValue.ValueLiteral is {} valueNode) + { + QueryableFilterContext filterContext = + argumentVisitor(valueNode, filterInputType, false); + + Expression instance = context.PopInstance(); + if (filterContext.Errors.Count == 0 && + filterContext.TryCreateLambda(out LambdaExpression? expression)) + { + context.PushInstance( + Expression.Call( + typeof(Enumerable), + nameof(Enumerable.Where), + new[] { filterInputType.EntityType.Source }, + instance, + (Expression)expression)); + } + else + { + context.PushInstance( + Expression.Constant(Array.CreateInstance(filterInputType.RuntimeType, 0))); + context.ReportError( + ProjectionProvider_CouldNotProjectFiltering(valueNode)); + } + } + } + + public void AfterProjection(QueryableProjectionContext context, ISelection selection) + { + } + } +} diff --git a/src/HotChocolate/Data/src/Data/Projections/Expressions/Interceptor/QueryableFirstOrDefaultInterceptor.cs b/src/HotChocolate/Data/src/Data/Projections/Expressions/Interceptor/QueryableFirstOrDefaultInterceptor.cs new file mode 100644 index 00000000000..b8ffaf27c92 --- /dev/null +++ b/src/HotChocolate/Data/src/Data/Projections/Expressions/Interceptor/QueryableFirstOrDefaultInterceptor.cs @@ -0,0 +1,10 @@ +namespace HotChocolate.Data.Projections.Handlers +{ + public class QueryableFirstOrDefaultInterceptor : QueryableTakeHandlerInterceptor + { + public QueryableFirstOrDefaultInterceptor() + : base(SelectionOptions.FirstOrDefault, 1) + { + } + } +} diff --git a/src/HotChocolate/Data/src/Data/Projections/Expressions/Interceptor/QueryableSingleOrDefaultInterceptor.cs b/src/HotChocolate/Data/src/Data/Projections/Expressions/Interceptor/QueryableSingleOrDefaultInterceptor.cs new file mode 100644 index 00000000000..250c13336ce --- /dev/null +++ b/src/HotChocolate/Data/src/Data/Projections/Expressions/Interceptor/QueryableSingleOrDefaultInterceptor.cs @@ -0,0 +1,11 @@ +namespace HotChocolate.Data.Projections.Handlers +{ + public class QueryableSingleOrDefaultInterceptor + : QueryableTakeHandlerInterceptor + { + public QueryableSingleOrDefaultInterceptor() + : base(SelectionOptions.SingleOrDefault, 2) + { + } + } +} diff --git a/src/HotChocolate/Data/src/Data/Projections/Expressions/Interceptor/QueryableSortInterceptor.cs b/src/HotChocolate/Data/src/Data/Projections/Expressions/Interceptor/QueryableSortInterceptor.cs new file mode 100644 index 00000000000..fd575450ea5 --- /dev/null +++ b/src/HotChocolate/Data/src/Data/Projections/Expressions/Interceptor/QueryableSortInterceptor.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using System.Linq.Expressions; +using HotChocolate.Data.Projections.Expressions; +using HotChocolate.Data.Projections.Expressions.Handlers; +using HotChocolate.Data.Sorting; +using HotChocolate.Data.Sorting.Expressions; +using HotChocolate.Execution.Processing; +using HotChocolate.Types; +using static HotChocolate.Data.ErrorHelper; +using static HotChocolate.Data.Sorting.Expressions.QueryableSortProvider; + +namespace HotChocolate.Data.Projections.Handlers +{ + public class QueryableSortInterceptor + : IProjectionFieldInterceptor + { + public bool CanHandle(ISelection selection) => + selection.Field.Member is {} && + selection.Field.ContextData.ContainsKey(ContextVisitSortArgumentKey) && + selection.Field.ContextData.ContainsKey(ContextArgumentNameKey); + + public void BeforeProjection( + QueryableProjectionContext context, + ISelection selection) + { + IObjectField field = selection.Field; + IReadOnlyDictionary contextData = field.ContextData; + + if (contextData.TryGetValue(ContextArgumentNameKey, out object? arg) && + arg is NameString argumentName && + contextData.TryGetValue(ContextVisitSortArgumentKey, out object? argVisitor) && + argVisitor is VisitSortArgument argumentVisitor && + context.Selection.Count > 0 && + context.Selection.Peek() + .Arguments.TryCoerceArguments( + context.Context.Variables, + context.Context.ReportError, + out IReadOnlyDictionary? coercedArgs) && + coercedArgs.TryGetValue(argumentName, out var argumentValue) && + argumentValue.Argument.Type is ListType lt && + lt.ElementType is ISortInputType sortInputType && + argumentValue.ValueLiteral is {} valueNode) + { + QueryableSortContext sortContext = + argumentVisitor(valueNode, sortInputType, false); + + Expression instance = context.PopInstance(); + if (sortContext.Errors.Count == 0) + { + context.PushInstance(sortContext.Compile(instance)); + } + else + { + context.PushInstance( + Expression.Constant(Array.CreateInstance(sortInputType.RuntimeType, 0))); + context.ReportError( + ProjectionProvider_CouldNotProjectSorting(valueNode)); + } + } + } + + public void AfterProjection(QueryableProjectionContext context, ISelection selection) + { + } + } +} diff --git a/src/HotChocolate/Data/src/Data/Projections/Expressions/Interceptor/QueryableTakeHandlerInterceptor.cs b/src/HotChocolate/Data/src/Data/Projections/Expressions/Interceptor/QueryableTakeHandlerInterceptor.cs new file mode 100644 index 00000000000..4059e0c4dc9 --- /dev/null +++ b/src/HotChocolate/Data/src/Data/Projections/Expressions/Interceptor/QueryableTakeHandlerInterceptor.cs @@ -0,0 +1,54 @@ +using System; +using System.Linq; +using System.Linq.Expressions; +using HotChocolate.Data.Projections.Expressions; +using HotChocolate.Data.Projections.Expressions.Handlers; +using HotChocolate.Execution.Processing; +using HotChocolate.Types; + +namespace HotChocolate.Data.Projections.Handlers +{ + public abstract class QueryableTakeHandlerInterceptor + : IProjectionFieldInterceptor + { + private readonly string _contextDataKey; + private readonly int _take; + + public QueryableTakeHandlerInterceptor(string contextDataKey, int take) + { + _contextDataKey = contextDataKey; + _take = take; + } + + public bool CanHandle(ISelection selection) => + selection.Field.Member is {} && + selection.Field.ContextData.ContainsKey(_contextDataKey); + + public void BeforeProjection( + QueryableProjectionContext context, + ISelection selection) + { + var field = selection.Field; + if (field.ContextData.ContainsKey(_contextDataKey) && + selection.Field.Type.InnerType() is ListType lt && + lt.ElementType.InnerType() is {} elementType) + { + Expression instance = context.PopInstance(); + + context.PushInstance( + Expression.Call( + typeof(Enumerable), + nameof(Enumerable.Take), + new[] { elementType.ToRuntimeType() }, + instance, + Expression.Constant(_take))); + } + } + + public void AfterProjection( + QueryableProjectionContext context, + ISelection selection) + { + } + } +} diff --git a/src/HotChocolate/Data/src/Data/Projections/Expressions/Optimizers/QueryableFilterProjectionOptimizer.cs b/src/HotChocolate/Data/src/Data/Projections/Expressions/Optimizers/QueryableFilterProjectionOptimizer.cs new file mode 100644 index 00000000000..8c63204a8c8 --- /dev/null +++ b/src/HotChocolate/Data/src/Data/Projections/Expressions/Optimizers/QueryableFilterProjectionOptimizer.cs @@ -0,0 +1,45 @@ +using HotChocolate.Execution.Processing; +using HotChocolate.Resolvers; +using static HotChocolate.Data.Filters.Expressions.QueryableFilterProvider; + +namespace HotChocolate.Data.Projections.Handlers +{ + public class QueryableFilterProjectionOptimizer : IProjectionOptimizer + { + public bool CanHandle(ISelection field) => + field.Field.Member is {} && + field.Field.ContextData.ContainsKey(ContextVisitFilterArgumentKey) && + field.Field.ContextData.ContainsKey(ContextArgumentNameKey); + + public Selection RewriteSelection( + SelectionOptimizerContext context, + Selection selection) + { + FieldDelegate resolverPipeline = + context.CompileResolverPipeline(selection.Field, selection.SyntaxNode); + + static FieldDelegate WrappedPipeline(FieldDelegate next) => + ctx => + { + ctx.LocalContextData = ctx.LocalContextData.SetItem( + SkipFilteringKey, + true); + + return next(ctx); + }; + + resolverPipeline = WrappedPipeline(resolverPipeline); + + var compiledSelection = new Selection( + context.Type, + selection.Field, + selection.SyntaxNode, + resolverPipeline, + arguments: selection.Arguments, + internalSelection: false); + + context.Fields[compiledSelection.ResponseName] = compiledSelection; + return compiledSelection; + } + } +} diff --git a/src/HotChocolate/Data/src/Data/Projections/Expressions/Optimizers/QueryablePagingProjectionOptimizer.cs b/src/HotChocolate/Data/src/Data/Projections/Expressions/Optimizers/QueryablePagingProjectionOptimizer.cs new file mode 100644 index 00000000000..7b20c46f72d --- /dev/null +++ b/src/HotChocolate/Data/src/Data/Projections/Expressions/Optimizers/QueryablePagingProjectionOptimizer.cs @@ -0,0 +1,94 @@ +using System; +using System.Collections.Generic; +using HotChocolate.Execution.Processing; +using HotChocolate.Language; +using HotChocolate.Resolvers; +using HotChocolate.Types; +using HotChocolate.Types.Pagination; + +namespace HotChocolate.Data.Projections.Handlers +{ + public class QueryablePagingProjectionOptimizer : IProjectionOptimizer + { + public bool CanHandle(ISelection field) => + field.Field.Member is {} && + field.DeclaringType is IPageType && + field.Field.Name.Value is "edges"; + + public Selection RewriteSelection( + SelectionOptimizerContext context, + Selection selection) + { + if (!(context.Type is IPageType pageType && + pageType.ItemType is ObjectType itemType)) + { + throw new InvalidOperationException(); + } + + context.Fields.TryGetValue("nodes", out Selection? nodeSelection); + context.Fields.TryGetValue("edges", out Selection? edgeSelection); + + + var selections = new List(); + if (edgeSelection?.SelectionSet is not null) + { + foreach (var edgeSubField in edgeSelection.SelectionSet.Selections) + { + if (edgeSubField is FieldNode edgeSubFieldNode && + edgeSubFieldNode.Name.Value is "node" && + edgeSubFieldNode.SelectionSet?.Selections is not null) + { + foreach (var nodeField in edgeSubFieldNode.SelectionSet.Selections) + { + if (nodeField is FieldNode nodeFieldNode) + { + selections.Add(nodeFieldNode); + } + } + } + } + } + + if (nodeSelection is null) + { + IObjectField nodesField = pageType.Fields["nodes"]; + var nodesFieldNode = new FieldNode( + null, + new NameNode("nodes"), + null, + Array.Empty(), + Array.Empty(), + new SelectionSetNode(selections)); + + FieldDelegate nodesPipeline = + context.CompileResolverPipeline(nodesField, nodesFieldNode); + + nodeSelection = new Selection( + itemType, + nodesField, + nodesFieldNode, + nodesPipeline, + arguments: selection.Arguments, + internalSelection: true); + } + else + { + if (nodeSelection.SelectionSet?.Selections is {}) + { + selections.AddRange(nodeSelection.SelectionSet.Selections); + } + + nodeSelection = new Selection( + itemType, + nodeSelection.Field, + nodeSelection.SyntaxNode.WithSelectionSet(new SelectionSetNode(selections)), + nodeSelection.ResolverPipeline, + arguments: selection.Arguments, + internalSelection: false); + } + + context.Fields["nodes"] = nodeSelection; + return selection; + } + } +} diff --git a/src/HotChocolate/Data/src/Data/Projections/Expressions/Optimizers/QueryableSortProjectionOptimizer.cs b/src/HotChocolate/Data/src/Data/Projections/Expressions/Optimizers/QueryableSortProjectionOptimizer.cs new file mode 100644 index 00000000000..35ef13cdee5 --- /dev/null +++ b/src/HotChocolate/Data/src/Data/Projections/Expressions/Optimizers/QueryableSortProjectionOptimizer.cs @@ -0,0 +1,42 @@ +using HotChocolate.Execution.Processing; +using HotChocolate.Resolvers; +using static HotChocolate.Data.Sorting.Expressions.QueryableSortProvider; + +namespace HotChocolate.Data.Projections.Handlers +{ + public class QueryableSortProjectionOptimizer : IProjectionOptimizer + { + public bool CanHandle(ISelection field) => + field.Field.Member is {} && + field.Field.ContextData.ContainsKey(ContextVisitSortArgumentKey) && + field.Field.ContextData.ContainsKey(ContextArgumentNameKey); + + public Selection RewriteSelection( + SelectionOptimizerContext context, + Selection selection) + { + FieldDelegate resolverPipeline = + context.CompileResolverPipeline(selection.Field, selection.SyntaxNode); + + static FieldDelegate WrappedPipeline(FieldDelegate next) => + ctx => + { + ctx.LocalContextData = ctx.LocalContextData.SetItem(SkipSortingKey, true); + return next(ctx); + }; + + resolverPipeline = WrappedPipeline(resolverPipeline); + + var compiledSelection = new Selection( + context.Type, + selection.Field, + selection.SyntaxNode, + resolverPipeline, + arguments: selection.Arguments, + internalSelection: false); + + context.Fields[compiledSelection.ResponseName] = compiledSelection; + return compiledSelection; + } + } +} diff --git a/src/HotChocolate/Data/src/Data/Projections/Expressions/ProjectionExpressionBuilder.cs b/src/HotChocolate/Data/src/Data/Projections/Expressions/ProjectionExpressionBuilder.cs new file mode 100644 index 00000000000..0cced8fe331 --- /dev/null +++ b/src/HotChocolate/Data/src/Data/Projections/Expressions/ProjectionExpressionBuilder.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq.Expressions; + +namespace HotChocolate.Data.Projections.Expressions +{ + internal static class ProjectionExpressionBuilder + { + public static MemberInitExpression CreateMemberInit( + Type type, + IEnumerable expressions) + { + NewExpression ctor = Expression.New(type); + return Expression.MemberInit(ctor, expressions); + } + } +} diff --git a/src/HotChocolate/Data/src/Data/Projections/Expressions/QueryableProjectionContext.cs b/src/HotChocolate/Data/src/Data/Projections/Expressions/QueryableProjectionContext.cs new file mode 100644 index 00000000000..230843c91ae --- /dev/null +++ b/src/HotChocolate/Data/src/Data/Projections/Expressions/QueryableProjectionContext.cs @@ -0,0 +1,18 @@ +using System; +using System.Linq.Expressions; +using HotChocolate.Resolvers; +using HotChocolate.Types; + +namespace HotChocolate.Data.Projections.Expressions +{ + public class QueryableProjectionContext : ProjectionVisitorContext + { + public QueryableProjectionContext( + IResolverContext context, + IOutputType initialType, + Type runtimeType) + : base(context, initialType, new QueryableProjectionScope(runtimeType, "_s1")) + { + } + } +} diff --git a/src/HotChocolate/Data/src/Data/Projections/Expressions/QueryableProjectionContextExtensions.cs b/src/HotChocolate/Data/src/Data/Projections/Expressions/QueryableProjectionContextExtensions.cs new file mode 100644 index 00000000000..997a88fffc6 --- /dev/null +++ b/src/HotChocolate/Data/src/Data/Projections/Expressions/QueryableProjectionContextExtensions.cs @@ -0,0 +1,46 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Linq.Expressions; +using static HotChocolate.Data.ThrowHelper; + +namespace HotChocolate.Data.Projections.Expressions +{ + public static class QueryableProjectionContextExtensions + { + public static QueryableProjectionScope AddScope( + this QueryableProjectionContext context, + Type runtimeType) + { + var parameterName = "p" + context.Scopes.Count; + var closure = + new QueryableProjectionScope(runtimeType, parameterName); + context.Scopes.Push(closure); + return closure; + } + + public static bool TryGetQueryableScope( + this QueryableProjectionContext ctx, + [NotNullWhen(true)] out QueryableProjectionScope? scope) + { + if (ctx.Scopes.Count > 0 && + ctx.Scopes.Peek() is QueryableProjectionScope queryableScope) + { + scope = queryableScope; + return true; + } + + scope = null; + return false; + } + + public static Expression> Project(this QueryableProjectionContext context) + { + if (context.TryGetQueryableScope(out QueryableProjectionScope? scope)) + { + return scope.Project(); + } + + throw ProjectionConvention_CouldNotProject(); + } + } +} diff --git a/src/HotChocolate/Data/src/Data/Projections/Expressions/QueryableProjectionProvider.cs b/src/HotChocolate/Data/src/Data/Projections/Expressions/QueryableProjectionProvider.cs new file mode 100644 index 00000000000..a8b36d9adff --- /dev/null +++ b/src/HotChocolate/Data/src/Data/Projections/Expressions/QueryableProjectionProvider.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using HotChocolate.Resolvers; + +namespace HotChocolate.Data.Projections.Expressions +{ + public class QueryableProjectionProvider + : ProjectionProvider + { + public QueryableProjectionProvider() + { + } + + public QueryableProjectionProvider( + Action configure) + : base(configure) + { + } + + public override FieldMiddleware CreateExecutor() + { + return next => context => ExecuteAsync(next, context); + + async ValueTask ExecuteAsync( + FieldDelegate next, + IMiddlewareContext context) + { + // first we let the pipeline run and produce a result. + await next(context).ConfigureAwait(false); + + + IQueryable? source = null; + + if (context.Result is IQueryable q) + { + source = q; + } + else if (context.Result is IEnumerable e) + { + source = e.AsQueryable(); + } + + if (source is not null) + { + var visitorContext = + new QueryableProjectionContext( + context, + context.ObjectType, + context.Field.Type.UnwrapRuntimeType()); + var visitor = new QueryableProjectionVisitor(); + visitor.Visit(visitorContext); + context.Result = source.Select(visitorContext.Project()); + } + } + } + } +} diff --git a/src/HotChocolate/Data/src/Data/Projections/Expressions/QueryableProjectionScope.cs b/src/HotChocolate/Data/src/Data/Projections/Expressions/QueryableProjectionScope.cs new file mode 100644 index 00000000000..4d022ba8e4e --- /dev/null +++ b/src/HotChocolate/Data/src/Data/Projections/Expressions/QueryableProjectionScope.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Linq.Expressions; +using HotChocolate.Language; + +namespace HotChocolate.Data.Projections.Expressions +{ + public class QueryableProjectionScope + : ProjectionScope + { + public QueryableProjectionScope( + Type type, + string parameterName) + { + Parameter = Expression.Parameter(type, parameterName); + Instance.Push(Parameter); + RuntimeType = type; + Level = new Stack>(); + Level.Push(new Queue()); + } + + public Type RuntimeType { get; } + + /// + /// Contains a queue for each level of the AST. The queues contain all operations of a level + /// A new queue is needed when entering new + /// + public Stack> Level { get; } + + public ParameterExpression Parameter { get; } + } +} diff --git a/src/HotChocolate/Data/src/Data/Projections/Expressions/QueryableProjectionScopeExtensions.cs b/src/HotChocolate/Data/src/Data/Projections/Expressions/QueryableProjectionScopeExtensions.cs new file mode 100644 index 00000000000..07e505ffbdb --- /dev/null +++ b/src/HotChocolate/Data/src/Data/Projections/Expressions/QueryableProjectionScopeExtensions.cs @@ -0,0 +1,114 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; + +namespace HotChocolate.Data.Projections.Expressions +{ + public static class QueryableProjectionScopeExtensions + { + public static Expression> Project(this QueryableProjectionScope scope) + { + return (Expression>)scope.CreateMemberInitLambda(); + } + + public static MemberInitExpression CreateMemberInit(this QueryableProjectionScope scope) + { + NewExpression ctor = Expression.New(scope.RuntimeType); + return Expression.MemberInit(ctor, scope.Level.Peek()); + } + + public static Expression CreateMemberInitLambda(this QueryableProjectionScope scope) + { + return Expression.Lambda(scope.CreateMemberInit(), scope.Parameter); + } + + public static Expression CreateSelection( + this QueryableProjectionScope scope, + Expression source, + Type sourceType) + { + MethodCallExpression selection = Expression.Call( + typeof(Enumerable), + nameof(Enumerable.Select), + new[] { scope.RuntimeType, scope.RuntimeType }, + source, + scope.CreateMemberInitLambda()); + + if (sourceType.IsArray) + { + return ToArray(scope, selection); + } + + if (TryGetSetType(sourceType, out Type? setType)) + { + return ToSet(selection, setType); + } + + return ToList(scope, selection); + } + + private static Expression ToArray(QueryableProjectionScope scope, Expression source) + { + return Expression.Call( + typeof(Enumerable), + nameof(Enumerable.ToArray), + new[] { scope.RuntimeType }, + source); + } + + private static Expression ToList(QueryableProjectionScope scope, Expression source) + { + return Expression.Call( + typeof(Enumerable), + nameof(Enumerable.ToList), + new[] { scope.RuntimeType }, + source); + } + + private static Expression ToSet( + Expression source, + Type setType) + { + Type typedGeneric = + setType.MakeGenericType(source.Type.GetGenericArguments()[0]); + + ConstructorInfo? ctor = + typedGeneric.GetConstructor(new[] { source.Type }); + + if (ctor is null) + { + throw new InvalidOperationException(); + } + + return Expression.New(ctor, source); + } + + private static bool TryGetSetType( + Type type, + [NotNullWhen(true)] out Type? setType) + { + if (type.IsGenericType) + { + Type typeDefinition = type.GetGenericTypeDefinition(); + if (typeDefinition == typeof(ISet<>) || + typeDefinition == typeof(HashSet<>)) + { + setType = typeof(HashSet<>); + return true; + } + + if (typeDefinition == typeof(SortedSet<>)) + { + setType = typeof(SortedSet<>); + return true; + } + } + + setType = default; + return false; + } + } +} diff --git a/src/HotChocolate/Data/src/Data/Projections/Expressions/QueryableProjectionVisitor.cs b/src/HotChocolate/Data/src/Data/Projections/Expressions/QueryableProjectionVisitor.cs new file mode 100644 index 00000000000..4cf65b3728a --- /dev/null +++ b/src/HotChocolate/Data/src/Data/Projections/Expressions/QueryableProjectionVisitor.cs @@ -0,0 +1,7 @@ +namespace HotChocolate.Data.Projections.Expressions +{ + public class QueryableProjectionVisitor + : ProjectionVisitor + { + } +} diff --git a/src/HotChocolate/Data/src/Data/Projections/Expressions/TypeExtensions.cs b/src/HotChocolate/Data/src/Data/Projections/Expressions/TypeExtensions.cs new file mode 100644 index 00000000000..393760b93ab --- /dev/null +++ b/src/HotChocolate/Data/src/Data/Projections/Expressions/TypeExtensions.cs @@ -0,0 +1,20 @@ +using System; +using HotChocolate.Types; +using HotChocolate.Types.Pagination; + +namespace HotChocolate.Data.Projections.Expressions +{ + public static class TypeExtensions + { + public static Type UnwrapRuntimeType(this IType type) => + type switch + { + ListType t => t.ElementType().ToRuntimeType(), + IPageType t => t.ItemType.UnwrapRuntimeType(), + IEdgeType t => t.EntityType.UnwrapRuntimeType(), + NonNullType t => t.InnerType().UnwrapRuntimeType(), + ObjectType t => t.ToRuntimeType(), + _ => throw new InvalidOperationException() + }; + } +} diff --git a/src/HotChocolate/Data/src/Data/Projections/Extensions/FilterDescriptorContextExtensions.cs b/src/HotChocolate/Data/src/Data/Projections/Extensions/FilterDescriptorContextExtensions.cs new file mode 100644 index 00000000000..b04bbe36994 --- /dev/null +++ b/src/HotChocolate/Data/src/Data/Projections/Extensions/FilterDescriptorContextExtensions.cs @@ -0,0 +1,21 @@ +using System; +using HotChocolate.Configuration; +using HotChocolate.Types.Descriptors; + +namespace HotChocolate.Data.Projections +{ + public static class ProjectionDescriptorContextExtensions + { + public static IProjectionProvider GetProjectionConvention( + this ITypeSystemObjectContext context, + string? scope = null) => + context.DescriptorContext.GetProjectionConvention(scope); + + public static IProjectionProvider GetProjectionConvention( + this IDescriptorContext context, + string? scope = null) => + context.GetConventionOrDefault( + () => throw new Exception(), + scope); + } +} diff --git a/src/HotChocolate/Data/src/Data/Projections/Extensions/OutputFieldExtensions.cs b/src/HotChocolate/Data/src/Data/Projections/Extensions/OutputFieldExtensions.cs new file mode 100644 index 00000000000..0783913b480 --- /dev/null +++ b/src/HotChocolate/Data/src/Data/Projections/Extensions/OutputFieldExtensions.cs @@ -0,0 +1,19 @@ +using HotChocolate.Types; +using HotChocolate.Types.Pagination; +using static HotChocolate.Data.Projections.ProjectionConvention; +using static HotChocolate.Data.Projections.ProjectionProvider; + +namespace HotChocolate.Data.Projections +{ + internal static class OutputFieldExtensions + { + public static bool IsNotProjected(this IOutputField field) => + field.ContextData.TryGetValue(IsProjectedKey, out object? isProjectedObject) && + isProjectedObject is bool isProjected && !isProjected; + + public static bool HasProjectionMiddleware(this IOutputField field) => + (field.Type is INullableType nt && nt.InnerType() is IPageType) || + field.Type is IPageType || + field.ContextData.ContainsKey(ProjectionContextIdentifier); + } +} diff --git a/src/HotChocolate/Data/src/Data/Projections/Extensions/SchemaBuilderExtensions.cs b/src/HotChocolate/Data/src/Data/Projections/Extensions/SchemaBuilderExtensions.cs new file mode 100644 index 00000000000..a1931393b80 --- /dev/null +++ b/src/HotChocolate/Data/src/Data/Projections/Extensions/SchemaBuilderExtensions.cs @@ -0,0 +1,71 @@ +using System; + +namespace HotChocolate.Data.Projections +{ + /// + /// Provides filtering extensions for the . + /// + public static class SchemaBuilderExtensions + { + /// + /// Adds filtering support. + /// + /// + /// The . + /// + /// + /// Returns the . + /// + public static ISchemaBuilder AddProjections( + this ISchemaBuilder builder) => + AddProjections(builder, x => x.AddDefaults()); + + /// + /// Adds filtering support. + /// + /// + /// The . + /// + /// + /// Configures the convention. + /// + /// + /// The filter convention name. + /// + /// + /// Returns the . + /// + public static ISchemaBuilder AddProjections( + this ISchemaBuilder builder, + Action configure, + string? name = null) => + builder + .TryAddTypeInterceptor() + .TryAddConvention( + sp => new ProjectionConvention(configure), + name); + + /// + /// Adds filtering support. + /// + /// + /// The . + /// + /// + /// The filter convention name. + /// + /// + /// The concrete filter convention type. + /// + /// + /// Returns the . + /// + public static ISchemaBuilder AddProjections( + this ISchemaBuilder builder, + string? name = null) + where TConvention : class, IProjectionConvention => + builder + .TryAddTypeInterceptor() + .TryAddConvention(name); + } +} diff --git a/src/HotChocolate/Data/src/Data/Projections/Extensions/SelectionObjectFieldDescriptorExtensions.cs b/src/HotChocolate/Data/src/Data/Projections/Extensions/SelectionObjectFieldDescriptorExtensions.cs new file mode 100644 index 00000000000..f0e86d72827 --- /dev/null +++ b/src/HotChocolate/Data/src/Data/Projections/Extensions/SelectionObjectFieldDescriptorExtensions.cs @@ -0,0 +1,176 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using HotChocolate.Configuration; +using HotChocolate.Internal; +using HotChocolate.Resolvers; +using HotChocolate.Types.Descriptors; +using HotChocolate.Types.Descriptors.Definitions; +using HotChocolate.Data.Projections; +using HotChocolate.Execution; +using HotChocolate.Execution.Processing; +using HotChocolate.Language; +using static HotChocolate.Data.Projections.ProjectionProvider; +using static HotChocolate.Execution.Processing.SelectionOptimizerHelper; + +namespace HotChocolate.Types +{ + public class ProjectionOptimizer : ISelectionOptimizer + { + private readonly IProjectionProvider _convention; + + public ProjectionOptimizer( + IProjectionProvider convention) + { + _convention = convention; + } + + public void OptimizeSelectionSet(SelectionOptimizerContext context) + { + var processedFields = new HashSet(); + while (!processedFields.SetEquals(context.Fields.Keys)) + { + var fieldsToProcess = new HashSet(context.Fields.Keys); + fieldsToProcess.ExceptWith(processedFields); + foreach (var field in fieldsToProcess) + { + context.Fields[field] = + _convention.RewriteSelection(context, context.Fields[field]); + processedFields.Add(field); + } + } + } + + public bool AllowFragmentDeferral( + SelectionOptimizerContext context, + InlineFragmentNode fragment) + { + return false; + } + + public bool AllowFragmentDeferral( + SelectionOptimizerContext context, + FragmentSpreadNode fragmentSpread, + FragmentDefinitionNode fragmentDefinition) + { + return false; + } + } + + public static class ProjectionObjectFieldDescriptorExtensions + { + private static readonly MethodInfo _factoryTemplate = + typeof(ProjectionObjectFieldDescriptorExtensions) + .GetMethod(nameof(CreateMiddleware), BindingFlags.Static | BindingFlags.NonPublic)!; + + public static IObjectFieldDescriptor IsProjected( + this IObjectFieldDescriptor descriptor, + bool isProjected = true) + { + descriptor + .Extend() + .OnBeforeCreate( + x => x.ContextData[ProjectionConvention.IsProjectedKey] = isProjected); + + return descriptor; + } + + public static IObjectFieldDescriptor UseProjection( + this IObjectFieldDescriptor descriptor, + string? scope = null) + { + if (descriptor is null) + { + throw new ArgumentNullException(nameof(descriptor)); + } + + return UseProjection(descriptor, null, scope); + } + + public static IObjectFieldDescriptor UseProjection( + this IObjectFieldDescriptor descriptor, + string? scope = null) + { + if (descriptor is null) + { + throw new ArgumentNullException(nameof(descriptor)); + } + + return UseProjection(descriptor, typeof(T), scope); + } + + private static IObjectFieldDescriptor UseProjection( + IObjectFieldDescriptor descriptor, + Type? objectType, + string? scope = null) + { + FieldMiddleware placeholder = next => context => default; + + descriptor + .Use(placeholder) + .Extend() + .OnBeforeCreate( + (context, definition) => + { + Type? selectionType = objectType; + + if (selectionType is null) + { + if (definition.ResultType is null || + !context.TypeInspector.TryCreateTypeInfo( + definition.ResultType, + out ITypeInfo? typeInfo)) + { + throw new ArgumentException( + "Cannot handle the specified type.", + nameof(descriptor)); + } + + selectionType = typeInfo.NamedType; + } + + ILazyTypeConfiguration lazyConfiguration = + LazyTypeConfigurationBuilder + .New() + .Definition(definition) + .Configure( + (context, defintion) => + CompileMiddleware( + selectionType, + definition, + placeholder, + context, + scope)) + .On(ApplyConfigurationOn.Completion) + .Build(); + definition.Configurations.Add(lazyConfiguration); + }); + + return descriptor; + } + + private static void CompileMiddleware( + Type type, + ObjectFieldDefinition definition, + FieldMiddleware placeholder, + ITypeCompletionContext context, + string? scope) + { + IProjectionProvider convention = + context.DescriptorContext.GetProjectionConvention(scope); + RegisterOptimizer(definition.ContextData, new ProjectionOptimizer(convention)); + + definition.ContextData[ProjectionContextIdentifier] = true; + + MethodInfo factory = _factoryTemplate.MakeGenericMethod(type); + var middleware = (FieldMiddleware)factory.Invoke(null, new object[] { convention })!; + var index = definition.MiddlewareComponents.IndexOf(placeholder); + definition.MiddlewareComponents[index] = middleware; + } + + private static FieldMiddleware CreateMiddleware( + IProjectionProvider convention) => + convention.CreateExecutor(); + } +} diff --git a/src/HotChocolate/Data/src/Data/Projections/Extensions/SingleOrDefaultObjectFieldDescriptorExtensions.cs b/src/HotChocolate/Data/src/Data/Projections/Extensions/SingleOrDefaultObjectFieldDescriptorExtensions.cs new file mode 100644 index 00000000000..5477bdd7f02 --- /dev/null +++ b/src/HotChocolate/Data/src/Data/Projections/Extensions/SingleOrDefaultObjectFieldDescriptorExtensions.cs @@ -0,0 +1,111 @@ +using System; +using System.Threading.Tasks; +using HotChocolate.Internal; +using HotChocolate.Resolvers; +using HotChocolate.Types.Descriptors; +using HotChocolate.Types.Descriptors.Definitions; +using HotChocolate.Data.Projections; +using HotChocolate.Language; + +namespace HotChocolate.Types +{ + public static class SingleOrDefaultObjectFieldDescriptorExtensions + { + private static readonly Type _firstMiddleware = typeof(FirstOrDefaultMiddleware<>); + private static readonly Type _singleMiddleware = typeof(SingleOrDefaultMiddleware<>); + + public static IObjectFieldDescriptor UseFirstOrDefault( + this IObjectFieldDescriptor descriptor) => + ApplyMiddleware(descriptor, SelectionOptions.FirstOrDefault, _firstMiddleware); + + public static IObjectFieldDescriptor UseSingleOrDefault( + this IObjectFieldDescriptor descriptor) => + ApplyMiddleware(descriptor, SelectionOptions.SingleOrDefault, _singleMiddleware); + + private static IObjectFieldDescriptor ApplyMiddleware( + this IObjectFieldDescriptor descriptor, + string optionName, + Type middlewareDefinition) + { + if (descriptor is null) + { + throw new ArgumentNullException(nameof(descriptor)); + } + + FieldMiddleware placeholder = next => context => default; + + descriptor + .Use(placeholder) + .Extend() + .OnBeforeCreate( + (context, definition) => + { + definition.ContextData[optionName] = null; + + if (definition.ResultType is null || + !context.TypeInspector.TryCreateTypeInfo( + definition.ResultType, + out ITypeInfo? typeInfo)) + { + Type resultType = definition.ResolverType ?? typeof(object); + throw new ArgumentException( + $"Cannot handle the specified type `{resultType.FullName}`.", + nameof(descriptor)); + } + + Type selectionType = typeInfo.NamedType; + definition.ResultType = selectionType; + definition.Type = RewriteToNonNullableType( + context.TypeInspector, + definition.Type); + + ILazyTypeConfiguration lazyConfiguration = + LazyTypeConfigurationBuilder + .New() + .Definition(definition) + .Configure( + (_, __) => + { + CompileMiddleware( + selectionType, + definition, + placeholder, + middlewareDefinition); + }) + .On(ApplyConfigurationOn.Completion) + .Build(); + + definition.Configurations.Add(lazyConfiguration); + }); + + return descriptor; + } + + private static void CompileMiddleware( + Type type, + ObjectFieldDefinition definition, + FieldMiddleware placeholder, + Type middlewareDefinition) + { + Type middlewareType = middlewareDefinition.MakeGenericType(type); + FieldMiddleware middleware = FieldClassMiddlewareFactory.Create(middlewareType); + var index = definition.MiddlewareComponents.IndexOf(placeholder); + definition.MiddlewareComponents[index] = middleware; + } + + private static ITypeReference RewriteToNonNullableType( + ITypeInspector typeInspector, + ITypeReference reference) + { + if (reference is ExtendedTypeReference extendedTypeRef) + { + return extendedTypeRef.Type.IsNullable + ? extendedTypeRef.WithType( + typeInspector.ChangeNullability(extendedTypeRef.Type, false)) + : extendedTypeRef; + } + + throw new NotSupportedException(); + } + } +} diff --git a/src/HotChocolate/Data/src/Data/Projections/FirstOrDefaultMiddleware~1.cs b/src/HotChocolate/Data/src/Data/Projections/FirstOrDefaultMiddleware~1.cs new file mode 100644 index 00000000000..b1cbcf7567e --- /dev/null +++ b/src/HotChocolate/Data/src/Data/Projections/FirstOrDefaultMiddleware~1.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using HotChocolate.Resolvers; + +namespace HotChocolate.Data.Projections +{ + public sealed class FirstOrDefaultMiddleware + { + public const string ContextKey = nameof(FirstOrDefaultMiddleware); + + private readonly FieldDelegate _next; + + public FirstOrDefaultMiddleware(FieldDelegate next) + { + _next = next ?? throw new ArgumentNullException(nameof(next)); + } + + public async Task InvokeAsync(IMiddlewareContext context) + { + await _next(context).ConfigureAwait(false); + + if (context.Result is IAsyncEnumerable ae) + { + await using IAsyncEnumerator enumerator = + ae.GetAsyncEnumerator(context.RequestAborted); + + if (await enumerator.MoveNextAsync().ConfigureAwait(false)) + { + context.Result = enumerator.Current; + } + else + { + context.Result = default(T)!; + } + } + else if (context.Result is IEnumerable e) + { + context.Result = await Task + .Run(() => e.FirstOrDefault(), context.RequestAborted) + .ConfigureAwait(false); + } + } + } +} diff --git a/src/HotChocolate/Data/src/Data/Projections/HotChocolate.Types.Selections.csproj b/src/HotChocolate/Data/src/Data/Projections/HotChocolate.Types.Selections.csproj new file mode 100644 index 00000000000..a7b2d1dfd6d --- /dev/null +++ b/src/HotChocolate/Data/src/Data/Projections/HotChocolate.Types.Selections.csproj @@ -0,0 +1,17 @@ + + + + HotChocolate.Data.Projections + HotChocolate.Data.Projections + HotChocolate.Data.Projections + enable + + + + + + + + + + diff --git a/src/HotChocolate/Data/src/Data/Projections/IProjectionSelection.cs b/src/HotChocolate/Data/src/Data/Projections/IProjectionSelection.cs new file mode 100644 index 00000000000..b6fe7f599c1 --- /dev/null +++ b/src/HotChocolate/Data/src/Data/Projections/IProjectionSelection.cs @@ -0,0 +1,9 @@ +using HotChocolate.Execution.Processing; + +namespace HotChocolate.Data.Projections +{ + public interface IProjectionSelection : ISelection + { + IProjectionFieldHandler Handler { get; } + } +} diff --git a/src/HotChocolate/Data/src/Data/Projections/ISelectionVisitorContext.cs b/src/HotChocolate/Data/src/Data/Projections/ISelectionVisitorContext.cs new file mode 100644 index 00000000000..7524f211050 --- /dev/null +++ b/src/HotChocolate/Data/src/Data/Projections/ISelectionVisitorContext.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using HotChocolate.Execution.Processing; +using HotChocolate.Language; +using HotChocolate.Resolvers; +using HotChocolate.Types; + +namespace HotChocolate.Data.Projections +{ + public interface ISelectionVisitorContext + { + Stack Selection { get; } + + Stack SelectionSetNodes { get; } + + IResolverContext Context { get; } + } +} diff --git a/src/HotChocolate/Data/src/Data/Projections/Optimizers/IsProjectedProjectionOptimizer.cs b/src/HotChocolate/Data/src/Data/Projections/Optimizers/IsProjectedProjectionOptimizer.cs new file mode 100644 index 00000000000..13bd0c170c8 --- /dev/null +++ b/src/HotChocolate/Data/src/Data/Projections/Optimizers/IsProjectedProjectionOptimizer.cs @@ -0,0 +1,56 @@ +using System; +using HotChocolate.Execution.Processing; +using HotChocolate.Language; +using HotChocolate.Resolvers; +using HotChocolate.Types; +using static HotChocolate.Data.Projections.ProjectionConvention; + +namespace HotChocolate.Data.Projections.Handlers +{ + public class IsProjectedProjectionOptimizer : IProjectionOptimizer + { + public bool CanHandle(ISelection field) => + field.DeclaringType is ObjectType objectType && + objectType.ContextData.ContainsKey(AlwaysProjectedFieldsKey); + + public Selection RewriteSelection( + SelectionOptimizerContext context, + Selection selection) + { + if (context.Type is ObjectType type && + type.ContextData.TryGetValue(AlwaysProjectedFieldsKey, out object? fieldsObj) && + fieldsObj is string[] fields) + { + for (var i = 0; i < fields.Length; i++) + { + if (!context.Fields.ContainsKey(fields[i])) + { + IObjectField nodesField = type.Fields[fields[i]]; + var nodesFieldNode = new FieldNode( + null, + new NameNode(fields[i]), + null, + Array.Empty(), + Array.Empty(), + null); + + FieldDelegate nodesPipeline = + context.CompileResolverPipeline(nodesField, nodesFieldNode); + + var compiledSelection = new Selection( + context.Type, + nodesField, + nodesFieldNode, + nodesPipeline, + arguments: selection.Arguments, + internalSelection: true); + + context.Fields[fields[i]] = compiledSelection; + } + } + } + + return selection; + } + } +} diff --git a/src/HotChocolate/Data/src/Data/Projections/ProjectionTypeInterceptor.cs b/src/HotChocolate/Data/src/Data/Projections/ProjectionTypeInterceptor.cs new file mode 100644 index 00000000000..506505a0dc8 --- /dev/null +++ b/src/HotChocolate/Data/src/Data/Projections/ProjectionTypeInterceptor.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; +using HotChocolate.Configuration; +using HotChocolate.Types.Descriptors.Definitions; +using static HotChocolate.Data.Projections.ProjectionConvention; + +namespace HotChocolate.Data.Projections +{ + public class ProjectionTypeInterceptor + : TypeInterceptor + { + public override bool CanHandle(ITypeSystemObjectContext context) => true; + + public override void OnAfterCompleteName( + ITypeCompletionContext completionContext, + DefinitionBase definition, + IDictionary contextData) + { + if (definition is ObjectTypeDefinition objectTypeDefinition) + { + List? alwaysProjected = null; + foreach (var field in objectTypeDefinition.Fields) + { + alwaysProjected ??= new List(); + if (field.ContextData.TryGetValue(IsProjectedKey, out object? isProjectedObj) && + isProjectedObj is bool isProjected && + isProjected) + { + alwaysProjected.Add(field.Name); + } + } + + if (alwaysProjected?.Count > 0) + { + definition.ContextData[AlwaysProjectedFieldsKey] = alwaysProjected.ToArray(); + } + } + } + } +} diff --git a/src/HotChocolate/Data/src/Data/Projections/ProjectionVisitor.cs b/src/HotChocolate/Data/src/Data/Projections/ProjectionVisitor.cs new file mode 100644 index 00000000000..616221f659f --- /dev/null +++ b/src/HotChocolate/Data/src/Data/Projections/ProjectionVisitor.cs @@ -0,0 +1,153 @@ +using System; +using System.Collections.Generic; +using HotChocolate.Language; +using HotChocolate.Execution.Processing; +using HotChocolate.Resolvers; +using HotChocolate.Types; +using HotChocolate.Types.Pagination; +using static HotChocolate.Data.Projections.ProjectionProvider; + +namespace HotChocolate.Data.Projections +{ + public class ProjectionVisitor + : SelectionVisitor + where TContext : IProjectionVisitorContext + { + public virtual void Visit(TContext context) + { + SelectionSetNode selectionSet = + context.Context.FieldSelection.SelectionSet ?? throw new Exception(); + context.SelectionSetNodes.Push(selectionSet); + Visit(context.Context.Field, context); + } + + protected override TContext OnBeforeLeave(ISelection selection, TContext localContext) + { + if (selection is IProjectionSelection projectionSelection && + projectionSelection.Handler is IProjectionFieldHandler handler) + { + return handler.OnBeforeLeave(localContext, selection); + } + + return localContext; + } + + protected override TContext OnAfterLeave( + ISelection selection, + TContext localContext, + ISelectionVisitorAction result) + { + if (selection is IProjectionSelection projectionSelection && + projectionSelection.Handler is IProjectionFieldHandler handler) + { + return handler.OnAfterLeave(localContext, selection, result); + } + + return localContext; + } + + protected override TContext OnAfterEnter( + ISelection selection, + TContext localContext, + ISelectionVisitorAction result) + { + if (selection is IProjectionSelection projectionSelection && + projectionSelection.Handler is IProjectionFieldHandler handler) + { + return handler.OnAfterEnter(localContext, selection, result); + } + + return localContext; + } + + protected override TContext OnBeforeEnter(ISelection selection, TContext context) + { + if (selection is IProjectionSelection projectionSelection && + projectionSelection.Handler is IProjectionFieldHandler handler) + { + return handler.OnBeforeEnter(context, selection); + } + + return context; + } + + protected override ISelectionVisitorAction Enter( + ISelection selection, + TContext context) + { + base.Enter(selection, context); + + if (selection is IProjectionSelection projectionSelection && + projectionSelection.Handler is IProjectionFieldHandler handler && + handler.TryHandleEnter( + context, + selection, + out ISelectionVisitorAction? handlerResult)) + { + return handlerResult; + } + + return SkipAndLeave; + } + + protected override ISelectionVisitorAction Leave( + ISelection selection, + TContext context) + { + base.Leave(selection, context); + + if (selection is IProjectionSelection projectionSelection && + projectionSelection.Handler is IProjectionFieldHandler handler && + handler.TryHandleLeave( + context, + selection, + out ISelectionVisitorAction? handlerResult)) + { + return handlerResult; + } + + return SkipAndLeave; + } + + protected override ISelectionVisitorAction Visit(ISelection selection, TContext context) + { + if (selection.Field.IsNotProjected()) + { + return Skip; + } + + return base.Visit(selection, context); + } + + protected override ISelectionVisitorAction Visit(IOutputField field, TContext context) + { + if (context.SelectionSetNodes.Count > 1 && field.HasProjectionMiddleware()) + { + return Skip; + } + + if (field.Type is IPageType and ObjectType pageType && + context.SelectionSetNodes.Peek() is {} pagingFieldSelection) + { + IReadOnlyList selections = + context.Context.GetSelections(pageType, pagingFieldSelection, true); + + foreach (var selection in selections) + { + if (selection.Field.Name.Value is "nodes" && + selection.SyntaxNode.SelectionSet is not null) + { + context.SelectionSetNodes.Push( + selection.SyntaxNode.SelectionSet); + + return base.Visit(selection.Field, context); + } + } + + return Skip; + } + + return base.Visit(field, context); + } + } +} diff --git a/src/HotChocolate/Data/src/Data/Projections/SelectionOptions.cs b/src/HotChocolate/Data/src/Data/Projections/SelectionOptions.cs new file mode 100644 index 00000000000..78578e386c2 --- /dev/null +++ b/src/HotChocolate/Data/src/Data/Projections/SelectionOptions.cs @@ -0,0 +1,8 @@ +namespace HotChocolate.Data.Projections +{ + internal static class SelectionOptions + { + public const string FirstOrDefault = "FirstOrDefault"; + public const string SingleOrDefault = "SingleOrDefault"; + } +} diff --git a/src/HotChocolate/Data/src/Data/Projections/SelectionVisitor.cs b/src/HotChocolate/Data/src/Data/Projections/SelectionVisitor.cs new file mode 100644 index 00000000000..f8743b65dfb --- /dev/null +++ b/src/HotChocolate/Data/src/Data/Projections/SelectionVisitor.cs @@ -0,0 +1,33 @@ +namespace HotChocolate.Data.Projections +{ + public class SelectionVisitor + { + /// + /// The visitor default action. + /// + /// + protected virtual ISelectionVisitorAction DefaultAction { get; } = Continue; + + /// + /// Ends traversing the graph. + /// + public static ISelectionVisitorAction Break { get; } = new BreakSelectionVisitorAction(); + + /// + /// Skips the child nodes and the current node. + /// + public static ISelectionVisitorAction Skip { get; } = new SkipSelectionVisitorAction(); + + /// + /// Continues traversing the graph. + /// + public static ISelectionVisitorAction Continue { get; } = + new ContinueSelectionVisitorAction(); + + /// + /// Skips the child node but completes the current node. + /// + public static ISelectionVisitorAction SkipAndLeave { get; } = + new SkipAndLeaveSelectionVisitorAction(); + } +} diff --git a/src/HotChocolate/Data/src/Data/Projections/SelectionVisitorContext.cs b/src/HotChocolate/Data/src/Data/Projections/SelectionVisitorContext.cs new file mode 100644 index 00000000000..68d8e66659a --- /dev/null +++ b/src/HotChocolate/Data/src/Data/Projections/SelectionVisitorContext.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using HotChocolate.Execution.Processing; +using HotChocolate.Language; +using HotChocolate.Resolvers; + +namespace HotChocolate.Data.Projections +{ + public class SelectionVisitorContext : ISelectionVisitorContext + { + public SelectionVisitorContext(IResolverContext context) + { + Selection = new Stack(); + SelectionSetNodes = new Stack(); + Context = context; + } + + public Stack Selection { get; } + + public Stack SelectionSetNodes { get; } + + public IResolverContext Context { get; } + } +} diff --git a/src/HotChocolate/Data/src/Data/Projections/SelectionVisitor`1.cs b/src/HotChocolate/Data/src/Data/Projections/SelectionVisitor`1.cs new file mode 100644 index 00000000000..151e7ab2d39 --- /dev/null +++ b/src/HotChocolate/Data/src/Data/Projections/SelectionVisitor`1.cs @@ -0,0 +1,198 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using HotChocolate.Execution.Processing; +using HotChocolate.Language; +using HotChocolate.Resolvers; +using HotChocolate.Types; + +namespace HotChocolate.Data.Projections +{ + public class SelectionVisitor + : SelectionVisitor + where TContext : ISelectionVisitorContext + { + protected virtual ISelectionVisitorAction Visit( + IOutputField field, + TContext context) + { + var localContext = OnBeforeEnter(field, context); + var result = Enter(field, localContext); + localContext = OnAfterEnter(field, localContext, result); + + if (result.Kind == SelectionVisitorActionKind.Continue) + { + if (VisitChildren(field, context).Kind == SelectionVisitorActionKind.Break) + { + return Break; + } + } + + if (result.Kind == SelectionVisitorActionKind.Continue || + result.Kind == SelectionVisitorActionKind.SkipAndLeave) + { + localContext = OnBeforeLeave(field, localContext); + result = Leave(field, localContext); + OnAfterLeave(field, localContext, result); + } + + return result; + } + + protected virtual TContext OnBeforeLeave( + IOutputField field, + TContext localContext) => + localContext; + + protected virtual TContext OnAfterLeave( + IOutputField field, + TContext localContext, + ISelectionVisitorAction result) => + localContext; + + protected virtual TContext OnAfterEnter( + IOutputField field, + TContext localContext, + ISelectionVisitorAction result) => + localContext; + + protected virtual TContext OnBeforeEnter( + IOutputField field, + TContext context) => + context; + + protected virtual ISelectionVisitorAction Visit( + ISelection selection, + TContext context) + { + var localContext = OnBeforeEnter(selection, context); + ISelectionVisitorAction result = Enter(selection, localContext); + localContext = OnAfterEnter(selection, localContext, result); + + if (result.Kind == SelectionVisitorActionKind.Continue) + { + if (VisitChildren(selection, context).Kind == SelectionVisitorActionKind.Break) + { + return Break; + } + } + + if (result.Kind == SelectionVisitorActionKind.Continue || + result.Kind == SelectionVisitorActionKind.SkipAndLeave) + { + localContext = OnBeforeLeave(selection, localContext); + result = Leave(selection, localContext); + OnAfterLeave(selection, localContext, result); + } + + return result; + } + + protected virtual TContext OnBeforeLeave( + ISelection selection, + TContext localContext) => + localContext; + + protected virtual TContext OnAfterLeave( + ISelection selection, + TContext localContext, + ISelectionVisitorAction result) => + localContext; + + protected virtual TContext OnAfterEnter( + ISelection selection, + TContext localContext, + ISelectionVisitorAction result) => + localContext; + + protected virtual TContext OnBeforeEnter( + ISelection selection, + TContext context) => + context; + + protected virtual ISelectionVisitorAction VisitChildren( + IOutputField field, + TContext context) + { + IOutputType type = field.Type; + SelectionSetNode? selectionSet = + context.SelectionSetNodes.Peek(); + + if (TryGetObjectType(type, out ObjectType? objectType) && + selectionSet is not null) + { + IReadOnlyList selections = context.Context.GetSelections( + objectType, + selectionSet, + true); + + for (var i = 0; i < selections.Count; i++) + { + if (selections[i] is ISelection selection) + { + if (Visit(selection, context).Kind == SelectionVisitorActionKind.Break) + { + return Break; + } + } + } + } + + return DefaultAction; + } + + private bool TryGetObjectType( + IType type, + [NotNullWhen(true)] out ObjectType? objectType) + { + switch (type) + { + case NonNullType nonNullType: + return TryGetObjectType(nonNullType.NamedType(), out objectType); + case ObjectType objType: + objectType = objType; + return true; + case ListType listType: + return TryGetObjectType(listType.InnerType(), out objectType); + default: + objectType = null; + return false; + } + } + + protected virtual ISelectionVisitorAction VisitChildren( + ISelection selection, + TContext context) + { + IObjectField field = selection.Field; + return Visit(field, context); + } + + protected virtual ISelectionVisitorAction Enter( + IOutputField field, + TContext context) => + DefaultAction; + + protected virtual ISelectionVisitorAction Leave( + IOutputField field, + TContext context) => + DefaultAction; + + protected virtual ISelectionVisitorAction Enter( + ISelection selection, + TContext context) + { + context.Selection.Push(selection); + context.SelectionSetNodes.Push(selection.SelectionSet); + return DefaultAction; + } + + protected virtual ISelectionVisitorAction Leave( + ISelection selection, + TContext context) + { + context.Selection.Pop(); + context.SelectionSetNodes.Pop(); + return DefaultAction; + } + } +} diff --git a/src/HotChocolate/Data/src/Data/Projections/SingleOrDefaultMiddleware~1.cs b/src/HotChocolate/Data/src/Data/Projections/SingleOrDefaultMiddleware~1.cs new file mode 100644 index 00000000000..e2aab0c1acd --- /dev/null +++ b/src/HotChocolate/Data/src/Data/Projections/SingleOrDefaultMiddleware~1.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using HotChocolate.Resolvers; +using static HotChocolate.Data.ErrorHelper; + +namespace HotChocolate.Data.Projections +{ + public class SingleOrDefaultMiddleware + { + private readonly FieldDelegate _next; + + public SingleOrDefaultMiddleware(FieldDelegate next) + { + _next = next ?? throw new ArgumentNullException(nameof(next)); + } + + public async Task InvokeAsync(IMiddlewareContext context) + { + await _next(context).ConfigureAwait(false); + + if (context.Result is IAsyncEnumerable ae) + { + await using IAsyncEnumerator enumerator = + ae.GetAsyncEnumerator(context.RequestAborted); + + if (await enumerator.MoveNextAsync().ConfigureAwait(false)) + { + context.Result = enumerator.Current; + } + else + { + context.Result = default(T)!; + } + + if (await enumerator.MoveNextAsync().ConfigureAwait(false)) + { + context.Result = ProjectionProvider_CreateMoreThanOneError(context); + } + } + else if (context.Result is IEnumerable e) + { + context.Result = await Task + .Run( + () => + { + try + { + return e.SingleOrDefault(); + } + catch (InvalidOperationException) + { + return ProjectionProvider_CreateMoreThanOneError(context); + } + }, + context.RequestAborted) + .ConfigureAwait(false); + } + } + } +} diff --git a/src/HotChocolate/Data/src/Data/Projections/Visitor/Actions/BreakSyntaxVisitorAction.cs b/src/HotChocolate/Data/src/Data/Projections/Visitor/Actions/BreakSyntaxVisitorAction.cs new file mode 100644 index 00000000000..2a280670b65 --- /dev/null +++ b/src/HotChocolate/Data/src/Data/Projections/Visitor/Actions/BreakSyntaxVisitorAction.cs @@ -0,0 +1,7 @@ +namespace HotChocolate.Data.Projections +{ + public class BreakSelectionVisitorAction : ISelectionVisitorAction + { + public SelectionVisitorActionKind Kind => SelectionVisitorActionKind.Break; + } +} diff --git a/src/HotChocolate/Data/src/Data/Projections/Visitor/Actions/ContinueSyntaxVisitorAction.cs b/src/HotChocolate/Data/src/Data/Projections/Visitor/Actions/ContinueSyntaxVisitorAction.cs new file mode 100644 index 00000000000..015cc7fd2f6 --- /dev/null +++ b/src/HotChocolate/Data/src/Data/Projections/Visitor/Actions/ContinueSyntaxVisitorAction.cs @@ -0,0 +1,7 @@ +namespace HotChocolate.Data.Projections +{ + public class ContinueSelectionVisitorAction : ISelectionVisitorAction + { + public SelectionVisitorActionKind Kind => SelectionVisitorActionKind.Continue; + } +} diff --git a/src/HotChocolate/Data/src/Data/Projections/Visitor/Actions/SkipAndLeaveSyntaxVisitorAction.cs b/src/HotChocolate/Data/src/Data/Projections/Visitor/Actions/SkipAndLeaveSyntaxVisitorAction.cs new file mode 100644 index 00000000000..bb43d67bfcf --- /dev/null +++ b/src/HotChocolate/Data/src/Data/Projections/Visitor/Actions/SkipAndLeaveSyntaxVisitorAction.cs @@ -0,0 +1,7 @@ +namespace HotChocolate.Data.Projections +{ + public class SkipAndLeaveSelectionVisitorAction : ISelectionVisitorAction + { + public SelectionVisitorActionKind Kind => SelectionVisitorActionKind.SkipAndLeave; + } +} diff --git a/src/HotChocolate/Data/src/Data/Projections/Visitor/Actions/SkipSyntaxVisitorAction.cs b/src/HotChocolate/Data/src/Data/Projections/Visitor/Actions/SkipSyntaxVisitorAction.cs new file mode 100644 index 00000000000..f93197a3897 --- /dev/null +++ b/src/HotChocolate/Data/src/Data/Projections/Visitor/Actions/SkipSyntaxVisitorAction.cs @@ -0,0 +1,7 @@ +namespace HotChocolate.Data.Projections +{ + public class SkipSelectionVisitorAction : ISelectionVisitorAction + { + public SelectionVisitorActionKind Kind => SelectionVisitorActionKind.Skip; + } +} diff --git a/src/HotChocolate/Data/src/Data/Projections/Visitor/IProjectionFieldHandler.cs b/src/HotChocolate/Data/src/Data/Projections/Visitor/IProjectionFieldHandler.cs new file mode 100644 index 00000000000..96bcd1ec7cd --- /dev/null +++ b/src/HotChocolate/Data/src/Data/Projections/Visitor/IProjectionFieldHandler.cs @@ -0,0 +1,25 @@ +using HotChocolate.Execution.Processing; + +namespace HotChocolate.Data.Projections +{ + public interface IProjectionFieldHandler + { + /// + /// Tests if this field handle can handle a selection. If it can handle the selection it + /// will be attached to the compiled selection set on the + /// type + /// + /// The selection to test for + /// Returns true if the selection can be handled + bool CanHandle(ISelection selection); + + /// + /// Wrapped this field handler with a type interceptor + /// + /// + /// The interceptor that this handler should be wrapped with + /// + /// The wrapped handler + IProjectionFieldHandler Wrap(IProjectionFieldInterceptor interceptor); + } +} diff --git a/src/HotChocolate/Data/src/Data/Projections/Visitor/IProjectionFieldHandler~1.cs b/src/HotChocolate/Data/src/Data/Projections/Visitor/IProjectionFieldHandler~1.cs new file mode 100644 index 00000000000..b9698d6fde3 --- /dev/null +++ b/src/HotChocolate/Data/src/Data/Projections/Visitor/IProjectionFieldHandler~1.cs @@ -0,0 +1,96 @@ +using System.Diagnostics.CodeAnalysis; +using HotChocolate.Execution.Processing; + +namespace HotChocolate.Data.Projections +{ + public interface IProjectionFieldHandler + : IProjectionFieldHandler + where TContext : IProjectionVisitorContext + { + /// + /// This method is called before the visitor calls + /// + /// + /// The context of the + /// The current selection + /// + /// The instance of that is used in TryHandleEnter + /// + TContext OnBeforeEnter(TContext context, ISelection selection); + + /// + /// Tries to apply projection to the field. This method is called after + /// and before + /// + /// + /// The context of the + /// The current selection + /// + /// The that the visitor should + /// continue with + /// + /// If true is returned the action is used for further processing + bool TryHandleEnter( + TContext context, + ISelection selection, + [NotNullWhen(true)] out ISelectionVisitorAction? action); + + /// + /// This method is called after the visitor calls + /// + /// + /// The context of the + /// The current selection + /// The action produced by TryHandleEnter + /// + /// The instance of that is used in on leave + /// + TContext OnAfterEnter( + TContext context, + ISelection selection, + ISelectionVisitorAction result); + + /// + /// This method is called before the visitor calls + /// + /// + /// The context of the + /// The current selection + /// + /// The instance of that is used in TryHandleLeave + /// + TContext OnBeforeLeave(TContext context, ISelection selection); + + /// + /// Tries to apply projection to the field. This method is called after + /// and before + /// + /// + /// The context of the + /// The current selection + /// + /// The that the visitor should + /// continue with + /// + /// If true is returned the action is used for further processing + bool TryHandleLeave( + TContext context, + ISelection selection, + [NotNullWhen(true)] out ISelectionVisitorAction? action); + + /// + /// This method is called after the visitor calls + /// + /// + /// The context of the + /// The action produced by TryHandleLeave + /// The current selection + /// + /// The instance of that is used in TryHandleLeave + /// + TContext OnAfterLeave( + TContext context, + ISelection selection, + ISelectionVisitorAction result); + } +} diff --git a/src/HotChocolate/Data/src/Data/Projections/Visitor/IProjectionOptimizer.cs b/src/HotChocolate/Data/src/Data/Projections/Visitor/IProjectionOptimizer.cs new file mode 100644 index 00000000000..443b75075ce --- /dev/null +++ b/src/HotChocolate/Data/src/Data/Projections/Visitor/IProjectionOptimizer.cs @@ -0,0 +1,29 @@ +using HotChocolate.Execution.Processing; + +namespace HotChocolate.Data.Projections +{ + public interface IProjectionOptimizer + { + /// + /// Tests if this optimizer can handle a selection If it can handle the selection it + /// will be attached to the compiled selection set on the + /// type + /// + /// The selection to test for + /// Returns true if the selection can be handled + bool CanHandle(ISelection selection); + + /// + /// Rewrites a selection. In case nothing is rewritten, the + /// is returned + /// + /// The context of the + /// The current selection + /// + /// Returns either the original or a rewritten version of it + /// + Selection RewriteSelection( + SelectionOptimizerContext context, + Selection selection); + } +} diff --git a/src/HotChocolate/Data/src/Data/Projections/Visitor/IProjectionVisitorContext.cs b/src/HotChocolate/Data/src/Data/Projections/Visitor/IProjectionVisitorContext.cs new file mode 100644 index 00000000000..6359718b3f9 --- /dev/null +++ b/src/HotChocolate/Data/src/Data/Projections/Visitor/IProjectionVisitorContext.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; + +namespace HotChocolate.Data.Projections +{ + public interface IProjectionVisitorContext + : ISelectionVisitorContext + { + IList Errors { get; } + } +} diff --git a/src/HotChocolate/Data/src/Data/Projections/Visitor/IProjectionVisitorContext~1.cs b/src/HotChocolate/Data/src/Data/Projections/Visitor/IProjectionVisitorContext~1.cs new file mode 100644 index 00000000000..e5fda6e80e0 --- /dev/null +++ b/src/HotChocolate/Data/src/Data/Projections/Visitor/IProjectionVisitorContext~1.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; + +namespace HotChocolate.Data.Projections +{ + public interface IProjectionVisitorContext + : IProjectionVisitorContext + { + Stack> Scopes { get; } + } +} diff --git a/src/HotChocolate/Data/src/Data/Projections/Visitor/ISelectionVisitorAction.cs b/src/HotChocolate/Data/src/Data/Projections/Visitor/ISelectionVisitorAction.cs new file mode 100644 index 00000000000..e017da470e1 --- /dev/null +++ b/src/HotChocolate/Data/src/Data/Projections/Visitor/ISelectionVisitorAction.cs @@ -0,0 +1,7 @@ +namespace HotChocolate.Data.Projections +{ + public interface ISelectionVisitorAction + { + SelectionVisitorActionKind Kind { get; } + } +} diff --git a/src/HotChocolate/Data/src/Data/Projections/Visitor/ProjectionScope.cs b/src/HotChocolate/Data/src/Data/Projections/Visitor/ProjectionScope.cs new file mode 100644 index 00000000000..6b695b545ff --- /dev/null +++ b/src/HotChocolate/Data/src/Data/Projections/Visitor/ProjectionScope.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using HotChocolate.Language; + +namespace HotChocolate.Data.Projections +{ + public class ProjectionScope + { + public ProjectionScope() + { + Instance = new Stack(); + } + + /// + /// Stores the current instance. In case of an expression this would be x.Foo.Bar + /// + public Stack Instance { get; } + } +} diff --git a/src/HotChocolate/Data/src/Data/Projections/Visitor/ProjectionVisitorContext.cs b/src/HotChocolate/Data/src/Data/Projections/Visitor/ProjectionVisitorContext.cs new file mode 100644 index 00000000000..cadaa0e662c --- /dev/null +++ b/src/HotChocolate/Data/src/Data/Projections/Visitor/ProjectionVisitorContext.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using HotChocolate.Resolvers; +using HotChocolate.Types; + +namespace HotChocolate.Data.Projections +{ + public abstract class ProjectionVisitorContext + : SelectionVisitorContext, + IProjectionVisitorContext + { + protected ProjectionVisitorContext( + IResolverContext context, + IOutputType initialType, + ProjectionScope projectionScope) : base(context) + { + if (initialType is null) + { + throw new ArgumentNullException(nameof(initialType)); + } + + Types.Push(initialType); + Scopes = new Stack>(); + Scopes.Push(projectionScope); + } + + public Stack> Scopes { get; } + + public Stack Types { get; } = new Stack(); + + public IList Errors { get; } = new List(); + } +} diff --git a/src/HotChocolate/Data/src/Data/Projections/Visitor/SelectionVisitorActionKind.cs b/src/HotChocolate/Data/src/Data/Projections/Visitor/SelectionVisitorActionKind.cs new file mode 100644 index 00000000000..135165f1310 --- /dev/null +++ b/src/HotChocolate/Data/src/Data/Projections/Visitor/SelectionVisitorActionKind.cs @@ -0,0 +1,10 @@ +namespace HotChocolate.Data.Projections +{ + public enum SelectionVisitorActionKind + { + Continue, + Skip, + Break, + SkipAndLeave + } +} diff --git a/src/HotChocolate/Data/src/Data/Sorting/Convention/ISortConvention.cs b/src/HotChocolate/Data/src/Data/Sorting/Convention/ISortConvention.cs index b8ab6bbd648..9b3a70eef77 100644 --- a/src/HotChocolate/Data/src/Data/Sorting/Convention/ISortConvention.cs +++ b/src/HotChocolate/Data/src/Data/Sorting/Convention/ISortConvention.cs @@ -3,6 +3,7 @@ using System.Reflection; using HotChocolate.Configuration; using HotChocolate.Resolvers; +using HotChocolate.Types; using HotChocolate.Types.Descriptors; using HotChocolate.Types.Descriptors.Definitions; @@ -150,5 +151,14 @@ public interface ISortConvention : IConvention /// for the specified entity type. /// FieldMiddleware CreateExecutor(); + + /// + /// Configures the field where the filters are applied. This can be used to add context + /// data to the field. + /// + /// + /// the field descriptor where the filtering is applied + /// + void ConfigureField(IObjectFieldDescriptor fieldDescriptor); } } diff --git a/src/HotChocolate/Data/src/Data/Sorting/Convention/SortConvention.cs b/src/HotChocolate/Data/src/Data/Sorting/Convention/SortConvention.cs index 0d9b7572a6a..33f49cb9eb6 100644 --- a/src/HotChocolate/Data/src/Data/Sorting/Convention/SortConvention.cs +++ b/src/HotChocolate/Data/src/Data/Sorting/Convention/SortConvention.cs @@ -225,6 +225,9 @@ public NameString GetOperationName(int operation) public FieldMiddleware CreateExecutor() => _provider.CreateExecutor(_argumentName); + public virtual void ConfigureField(IObjectFieldDescriptor descriptor) => + _provider.ConfigureField(_argumentName, descriptor); + public bool TryGetOperationHandler( ITypeDiscoveryContext context, EnumTypeDefinition typeDefinition, diff --git a/src/HotChocolate/Data/src/Data/Sorting/Expressions/QueryableSortProvider.cs b/src/HotChocolate/Data/src/Data/Sorting/Expressions/QueryableSortProvider.cs index 6280f8a3ef9..e002e955940 100644 --- a/src/HotChocolate/Data/src/Data/Sorting/Expressions/QueryableSortProvider.cs +++ b/src/HotChocolate/Data/src/Data/Sorting/Expressions/QueryableSortProvider.cs @@ -11,6 +11,10 @@ namespace HotChocolate.Data.Sorting.Expressions public class QueryableSortProvider : SortProvider { + public const string ContextArgumentNameKey = "SortArgumentName"; + public const string ContextVisitSortArgumentKey = nameof(VisitSortArgument); + public const string SkipSortingKey = "SkipSorting"; + public QueryableSortProvider() { } @@ -40,7 +44,12 @@ public override FieldMiddleware CreateExecutor(NameString argumentN IValueNode sort = context.ArgumentLiteral(argumentName); // if no sort is defined we can stop here and yield back control. - if (sort.IsNull()) + if (sort.IsNull() || + (context.LocalContextData.TryGetValue( + SkipSortingKey, + out object? skipObject) && + skipObject is bool skip && + skip)) { return; } @@ -58,15 +67,17 @@ public override FieldMiddleware CreateExecutor(NameString argumentN if (source != null && argument.Type is ListType lt && - lt.ElementType is ISortInputType sortInput) + lt.ElementType is ISortInputType sortInput && + context.Field.ContextData.TryGetValue( + ContextVisitSortArgumentKey, + out object? executorObj) && + executorObj is VisitSortArgument executor) { - var visitorContext = new QueryableSortContext( + QueryableSortContext visitorContext = executor( + sort, sortInput, source is EnumerableQuery); - // rewrite GraphQL input object into expression tree. - Visitor.Visit(sort, visitorContext); - // compile expression tree if (visitorContext.Errors.Count > 0) { @@ -83,5 +94,38 @@ public override FieldMiddleware CreateExecutor(NameString argumentN } } } + + public override void ConfigureField( + NameString argumentName, + IObjectFieldDescriptor descriptor) + { + QueryableSortContext VisitSortArgumentExecutor( + IValueNode valueNode, + ISortInputType filterInput, + bool inMemory) + { + var visitorContext = new QueryableSortContext( + filterInput, + inMemory); + + // rewrite GraphQL input object into expression tree. + Visitor.Visit(valueNode, visitorContext); + + return visitorContext; + } + + descriptor.ConfigureContextData( + contextData => + { + contextData[ContextVisitSortArgumentKey] = + (VisitSortArgument)VisitSortArgumentExecutor; + contextData[ContextArgumentNameKey] = argumentName; + }); + } } + + public delegate QueryableSortContext VisitSortArgument( + IValueNode filterValueNode, + ISortInputType filterInputType, + bool inMemory); } diff --git a/src/HotChocolate/Data/src/Data/Sorting/Extensions/SortObjectFieldDescriptorExtensions.cs b/src/HotChocolate/Data/src/Data/Sorting/Extensions/SortObjectFieldDescriptorExtensions.cs index 7f732524832..ec50e257e81 100644 --- a/src/HotChocolate/Data/src/Data/Sorting/Extensions/SortObjectFieldDescriptorExtensions.cs +++ b/src/HotChocolate/Data/src/Data/Sorting/Extensions/SortObjectFieldDescriptorExtensions.cs @@ -135,72 +135,79 @@ public static class SortObjectFieldDescriptorExtensions descriptor .Use(placeholder) .Extend() - .OnBeforeCreate((c, definition) => - { - Type? argumentType = sortType; - - if (argumentType is null) + .OnBeforeCreate( + (c, definition) => { - if (definition.ResultType is null || - definition.ResultType == typeof(object) || - !c.TypeInspector.TryCreateTypeInfo( - definition.ResultType, out ITypeInfo? typeInfo)) + Type? argumentType = sortType; + + if (argumentType is null) { - throw new ArgumentException( - SortObjectFieldDescriptorExtensions_UseSorting_CannotHandleType, - nameof(descriptor)); + if (definition.ResultType is null || + definition.ResultType == typeof(object) || + !c.TypeInspector.TryCreateTypeInfo( + definition.ResultType, + out ITypeInfo? typeInfo)) + { + throw new ArgumentException( + SortObjectFieldDescriptorExtensions_UseSorting_CannotHandleType, + nameof(descriptor)); + } + + argumentType = typeof(SortInputType<>) + .MakeGenericType(typeInfo.NamedType); } - argumentType = typeof(SortInputType<>) - .MakeGenericType(typeInfo.NamedType); - } - - ITypeReference argumentTypeReference = sortTypeInstance is null - ? (ITypeReference)c.TypeInspector.GetTypeRef( - argumentType, - TypeContext.Input, - scope) - : TypeReference.Create(sortTypeInstance, scope); + ITypeReference argumentTypeReference = sortTypeInstance is null + ? (ITypeReference)c.TypeInspector.GetTypeRef( + argumentType, + TypeContext.Input, + scope) + : TypeReference.Create(sortTypeInstance, scope); - if (argumentType == typeof(object)) - { - throw SortObjectFieldDescriptorExtensions_CannotInfer(); - } + if (argumentType == typeof(object)) + { + throw SortObjectFieldDescriptorExtensions_CannotInfer(); + } - argumentType = typeof(ListType<>).MakeGenericType(argumentType); + argumentType = typeof(ListType<>).MakeGenericType(argumentType); - var argumentDefinition = new ArgumentDefinition - { - Name = argumentPlaceholder, - Type = c.TypeInspector.GetTypeRef(argumentType, TypeContext.Input, scope) - }; - definition.Arguments.Add(argumentDefinition); - - definition.Configurations.Add( - LazyTypeConfigurationBuilder - .New() - .Definition(definition) - .Configure((context, def) => - CompileMiddleware( - context, - def, - argumentTypeReference, - placeholder, - scope)) - .On(ApplyConfigurationOn.Completion) - .DependsOn(argumentTypeReference, true) - .Build()); - - definition.Configurations.Add( - LazyTypeConfigurationBuilder - .New() - .Definition(definition) - .Configure((context, _) => - argumentDefinition.Name = - context.GetSortConvention(scope).GetArgumentName()) - .On(ApplyConfigurationOn.Naming) - .Build()); - }); + var argumentDefinition = new ArgumentDefinition + { + Name = argumentPlaceholder, + Type = c.TypeInspector.GetTypeRef( + argumentType, + TypeContext.Input, + scope) + }; + definition.Arguments.Add(argumentDefinition); + + definition.Configurations.Add( + LazyTypeConfigurationBuilder + .New() + .Definition(definition) + .Configure( + (context, def) => + CompileMiddleware( + context, + def, + argumentTypeReference, + placeholder, + scope)) + .On(ApplyConfigurationOn.Completion) + .DependsOn(argumentTypeReference, true) + .Build()); + + definition.Configurations.Add( + LazyTypeConfigurationBuilder + .New() + .Definition(definition) + .Configure( + (context, _) => + argumentDefinition.Name = + context.GetSortConvention(scope).GetArgumentName()) + .On(ApplyConfigurationOn.Naming) + .Build()); + }); return descriptor; } @@ -215,6 +222,9 @@ public static class SortObjectFieldDescriptorExtensions ISortInputType type = context.GetType(argumentTypeReference); ISortConvention convention = context.DescriptorContext.GetSortConvention(scope); + var fieldDescriptor = ObjectFieldDescriptor.From(context.DescriptorContext, definition); + convention.ConfigureField(fieldDescriptor); + MethodInfo factory = _factoryTemplate.MakeGenericMethod(type.EntityType.Source); var middleware = (FieldMiddleware)factory.Invoke(null, new object[] { convention })!; var index = definition.MiddlewareComponents.IndexOf(placeholder); diff --git a/src/HotChocolate/Data/src/Data/Sorting/Visitor/ISortFieldHandler.cs b/src/HotChocolate/Data/src/Data/Sorting/Visitor/ISortFieldHandler.cs index 3f1da2b7573..d20d7b1e740 100644 --- a/src/HotChocolate/Data/src/Data/Sorting/Visitor/ISortFieldHandler.cs +++ b/src/HotChocolate/Data/src/Data/Sorting/Visitor/ISortFieldHandler.cs @@ -4,6 +4,14 @@ namespace HotChocolate.Data.Sorting { public interface ISortFieldHandler { + /// + /// Tests if this field handler can handle a field If it can handle the field it + /// will be attached to the + /// + /// The discovery context of the schema + /// The definition of the declaring type of the field + /// The definition of the field + /// Returns true if the field can be handled bool CanHandle( ITypeDiscoveryContext context, ISortInputTypeDefinition typeDefinition, diff --git a/src/HotChocolate/Data/src/Data/Sorting/Visitor/ISortFieldHandler~1.cs b/src/HotChocolate/Data/src/Data/Sorting/Visitor/ISortFieldHandler~1.cs index dba124d0786..107a84b1de5 100644 --- a/src/HotChocolate/Data/src/Data/Sorting/Visitor/ISortFieldHandler~1.cs +++ b/src/HotChocolate/Data/src/Data/Sorting/Visitor/ISortFieldHandler~1.cs @@ -8,12 +8,36 @@ public interface ISortFieldHandler : ISortFieldHandler where TContext : ISortVisitorContext { + /// + /// This method is called when the encounters a + /// field + /// + /// The of the visitor + /// The field that is currently being visited + /// The value node of this field + /// + /// The that the visitor should + /// continue with + /// + /// If true is returned the action is used for further processing bool TryHandleEnter( TContext context, ISortField field, ObjectFieldNode node, [NotNullWhen(true)] out ISyntaxVisitorAction? action); + /// + /// This method is called when the leaves a + /// field + /// + /// The of the visitor + /// The field that is currently being visited + /// The value node of this field + /// + /// The that the visitor should + /// continue with + /// + /// If true is returned the action is used for further processing bool TryHandleLeave( TContext context, ISortField field, diff --git a/src/HotChocolate/Data/src/Data/Sorting/Visitor/ISortOperationHandler.cs b/src/HotChocolate/Data/src/Data/Sorting/Visitor/ISortOperationHandler.cs index 8fab2452b35..05b92fdb5c0 100644 --- a/src/HotChocolate/Data/src/Data/Sorting/Visitor/ISortOperationHandler.cs +++ b/src/HotChocolate/Data/src/Data/Sorting/Visitor/ISortOperationHandler.cs @@ -5,6 +5,14 @@ namespace HotChocolate.Data.Sorting { public interface ISortOperationHandler { + /// + /// Tests if this operation handler can handle a field If it can handle the field it + /// will be attached to the + /// + /// The discovery context of the schema + /// The definition of the declaring type of the field + /// The definition of the field + /// Returns true if the field can be handled bool CanHandle( ITypeDiscoveryContext context, EnumTypeDefinition typeDefinition, diff --git a/src/HotChocolate/Data/src/Data/Sorting/Visitor/ISortProvider.cs b/src/HotChocolate/Data/src/Data/Sorting/Visitor/ISortProvider.cs index 74e453136df..a7196e1c16f 100644 --- a/src/HotChocolate/Data/src/Data/Sorting/Visitor/ISortProvider.cs +++ b/src/HotChocolate/Data/src/Data/Sorting/Visitor/ISortProvider.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using HotChocolate.Resolvers; +using HotChocolate.Types; namespace HotChocolate.Data.Sorting { @@ -10,6 +11,12 @@ public interface ISortProvider IReadOnlyCollection OperationHandlers { get; } FieldMiddleware CreateExecutor(NameString argumentName); + + /// + /// Configures the field where the filters are applied. This can be used to add context + /// data to the field. + /// + void ConfigureField(NameString argumentName, IObjectFieldDescriptor descriptor); } } diff --git a/src/HotChocolate/Data/src/Data/Sorting/Visitor/SortProvider.cs b/src/HotChocolate/Data/src/Data/Sorting/Visitor/SortProvider.cs index df11a37d889..662b8b8090c 100644 --- a/src/HotChocolate/Data/src/Data/Sorting/Visitor/SortProvider.cs +++ b/src/HotChocolate/Data/src/Data/Sorting/Visitor/SortProvider.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using HotChocolate.Resolvers; +using HotChocolate.Types; using HotChocolate.Types.Descriptors; using HotChocolate.Utilities; using static HotChocolate.Data.DataResources; @@ -122,5 +123,11 @@ protected override SortProviderDefinition CreateDefinition(IConventionContext co protected virtual void Configure(ISortProviderDescriptor descriptor) { } public abstract FieldMiddleware CreateExecutor(NameString argumentName); + + public virtual void ConfigureField( + NameString argumentName, + IObjectFieldDescriptor descriptor) + { + } } } diff --git a/src/HotChocolate/Data/src/Data/ThrowHelper.cs b/src/HotChocolate/Data/src/Data/ThrowHelper.cs index ecfbde416c6..2630650ce4b 100644 --- a/src/HotChocolate/Data/src/Data/ThrowHelper.cs +++ b/src/HotChocolate/Data/src/Data/ThrowHelper.cs @@ -1,6 +1,7 @@ using System; using System.Reflection; using HotChocolate.Data.Filters; +using HotChocolate.Data.Projections; using HotChocolate.Data.Sorting; using HotChocolate.Language; using HotChocolate.Types.Descriptors.Definitions; @@ -183,5 +184,34 @@ internal static class ThrowHelper .SetExtension(nameof(sortConvention), sortConvention) .SetExtension(nameof(sortOperation), sortOperation) .Build()); + + public static SchemaException ProjectionProvider_NoHandlersConfigured( + IProjectionProvider projectionConvention) => + new SchemaException( + SchemaErrorBuilder.New() + .SetMessage( + DataResources.ProjectionProvider_NoHandlersConfigured, + projectionConvention.GetType().FullName ?? + projectionConvention.GetType().Name) + .SetExtension(nameof(projectionConvention), projectionConvention) + .Build()); + + public static SchemaException ProjectionConvention_NoProviderFound( + Type convention, + string? scope) => + new SchemaException( + SchemaErrorBuilder.New() + .SetMessage( + DataResources.ProjectionConvention_NoProviderFound, + convention.FullName ?? convention.Name) + .SetExtension(nameof(scope), scope) + .Build()); + + public static SchemaException ProjectionConvention_CouldNotProject() => + new SchemaException( + SchemaErrorBuilder.New() + .SetMessage( + DataResources.ProjectionConvention_CouldNotProject) + .Build()); } } diff --git a/src/HotChocolate/Data/test/Data.Filters.Tests/Expression/FilterVisitorTestBase.cs b/src/HotChocolate/Data/test/Data.Filters.Tests/Expression/FilterVisitorTestBase.cs index 89a6e5a6e85..a14db33de31 100644 --- a/src/HotChocolate/Data/test/Data.Filters.Tests/Expression/FilterVisitorTestBase.cs +++ b/src/HotChocolate/Data/test/Data.Filters.Tests/Expression/FilterVisitorTestBase.cs @@ -10,7 +10,7 @@ public class FilterVisitorTestBase { convention ??= new FilterConvention( - x => x.AddDefaults().BindRuntimeType(typeof(TRuntimeType), type.GetType())); + x => x.AddDefaults().BindRuntimeType(typeof(TRuntimeType), type.GetType())); ISchemaBuilder builder = SchemaBuilder.New() .AddConvention(convention) diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/DatabaseContext.cs b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/DatabaseContext.cs new file mode 100644 index 00000000000..2032dc2eadd --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/DatabaseContext.cs @@ -0,0 +1,57 @@ +using System; +using System.IO; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; + +namespace HotChocolate.Data.Projections +{ + public class DatabaseContext : DbContext + where T : class + { + private readonly string _fileName; + private readonly Action? _onModelCreating; + private bool _disposed; + + public DatabaseContext( + string fileName, + Action? onModelCreating = null) + { + _fileName = fileName; + _onModelCreating = onModelCreating; + } + + public DbSet Data { get; set; } = default!; + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + _onModelCreating?.Invoke(modelBuilder); + } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + optionsBuilder.UseSqlite($"Data Source={_fileName}"); + } + + public override async ValueTask DisposeAsync() + { + await base.DisposeAsync(); + + if (!_disposed) + { + if (File.Exists(_fileName)) + { + try + { + File.Delete(_fileName); + } + catch + { + // we will ignore if we cannot delete it. + } + } + + _disposed = true; + } + } + } +} diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/Extensions/TestExtensions.cs b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/Extensions/TestExtensions.cs new file mode 100644 index 00000000000..6b79237b09e --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/Extensions/TestExtensions.cs @@ -0,0 +1,25 @@ +using HotChocolate.Execution; +using HotChocolate.Tests; +using Snapshooter; +using Snapshooter.Xunit; + +namespace HotChocolate.Data.Projections +{ + public static class TestExtensions + { + public static void MatchSqlSnapshot( + this IExecutionResult? result, + string snapshotName = "") + { + if (result is { }) + { + result.MatchSnapshot(snapshotName); + if (result.ContextData is { } && + result.ContextData.TryGetValue("sql", out object? queryResult)) + { + queryResult.MatchSnapshot(new SnapshotNameExtension(snapshotName + "_sql")); + } + } + } + } +} diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/HotChocolate.Data.Projections.SqlServer.Tests.csproj b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/HotChocolate.Data.Projections.SqlServer.Tests.csproj new file mode 100644 index 00000000000..8cf16e4c961 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/HotChocolate.Data.Projections.SqlServer.Tests.csproj @@ -0,0 +1,22 @@ + + + + HotChocolate.Data.Projections.SqlServer.Tests + HotChocolate.Data + net5.0 + + + + + + + + + + + + + + + + diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/ProjectionVisitorTestBase.cs b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/ProjectionVisitorTestBase.cs new file mode 100644 index 00000000000..2ed125530cf --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/ProjectionVisitorTestBase.cs @@ -0,0 +1,149 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using HotChocolate.Data.Filters; +using HotChocolate.Data.Projections.Expressions; +using HotChocolate.Data.Sorting; +using HotChocolate.Execution; +using HotChocolate.Execution.Configuration; +using HotChocolate.Resolvers; +using HotChocolate.Types; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; + +namespace HotChocolate.Data.Projections +{ + public class ProjectionVisitorTestBase + { + protected string? FileName { get; set; } = Guid.NewGuid().ToString("N") + ".db"; + + private Func> BuildResolver( + Action? onModelCreating = null, + params TResult[] results) + where TResult : class + { + if (FileName is null) + { + throw new InvalidOperationException(); + } + + var dbContext = new DatabaseContext(FileName, onModelCreating); + dbContext.Database.EnsureDeleted(); + dbContext.Database.EnsureCreated(); + dbContext.AddRange(results); + + try + { + dbContext.SaveChanges(); + } + catch (Exception ex) + { + } + + return ctx => dbContext.Data.AsQueryable(); + } + + protected T[] CreateEntity(params T[] entities) => entities; + + public IRequestExecutor CreateSchema( + TEntity[] entities, + ProjectionProvider? convention = null, + Action? onModelCreating = null, + bool usePaging = false, + ObjectType? objectType = null) + where TEntity : class + { + convention ??= new QueryableProjectionProvider( + x => x.AddDefaults()); + + Func> resolver = BuildResolver( + onModelCreating, + entities); + + ISchemaBuilder builder = SchemaBuilder.New(); + + if (objectType is {}) + { + builder.AddType(objectType); + } + + builder + .AddConvention(convention) + .AddProjections() + .AddFiltering() + .AddSorting() + .AddQueryType( + new ObjectType>( + c => + { + IObjectFieldDescriptor descriptor = c + .Name("Query") + .Field(x => x.Root) + .Resolver(resolver); + + if (usePaging) + { + descriptor.UsePaging>(); + } + + descriptor + .Use( + next => async context => + { + await next(context); + + if (context.Result is IQueryable queryable) + { + try + { + context.ContextData["sql"] = + queryable.ToQueryString(); + } + catch (Exception ex) + { + context.ContextData["sql"] = ex.Message; + } + + context.Result = await queryable.ToListAsync(); + } + }) + .UseFiltering() + .UseSorting() + .UseProjection(); + })); + + ISchema schema = builder.Create(); + + return new ServiceCollection() + .Configure( + Schema.DefaultName, + o => o.Schema = schema) + .AddGraphQL() + .UseRequest( + next => async context => + { + await next(context); + if (context.Result is IReadOnlyQueryResult result && + context.ContextData.TryGetValue("sql", out object? queryString)) + { + context.Result = + QueryResultBuilder + .FromResult(result) + .SetContextData("sql", queryString) + .Create(); + } + }) + .UseDefaultPipeline() + .Services + .BuildServiceProvider() + .GetRequiredService() + .GetRequestExecutorAsync() + .Result; + } + + public class StubObject + { + public T Root { get; set; } + } + } +} diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/QueryableFirstOrDefaultTests.cs b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/QueryableFirstOrDefaultTests.cs new file mode 100644 index 00000000000..23c0b00bd9f --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/QueryableFirstOrDefaultTests.cs @@ -0,0 +1,312 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using HotChocolate.Execution; +using Microsoft.EntityFrameworkCore; +using Xunit; + +namespace HotChocolate.Data.Projections.Expressions +{ + public class QueryableFirstOrDefaultTests + { + private static readonly Bar[] _barEntities = + { + new Bar + { + Foo = new Foo + { + BarShort = 12, + BarBool = true, + BarEnum = BarEnum.BAR, + BarString = "testatest", + NestedObject = + new BarDeep { Foo = new FooDeep { BarShort = 12, BarString = "a" } }, + ObjectArray = new List + { + new BarDeep { Foo = new FooDeep { BarShort = 12, BarString = "a" } } + } + } + }, + new Bar + { + Foo = new Foo + { + BarShort = 14, + BarBool = true, + BarEnum = BarEnum.BAZ, + BarString = "testbtest", + NestedObject = + new BarDeep { Foo = new FooDeep { BarShort = 12, BarString = "d" } }, + ObjectArray = new List + { + new BarDeep { Foo = new FooDeep { BarShort = 14, BarString = "d" } } + } + } + } + }; + + private static readonly BarNullable[] _barNullableEntities = + { + new BarNullable + { + Foo = new FooNullable + { + BarShort = 12, + BarBool = true, + BarEnum = BarEnum.BAR, + BarString = "testatest", + ObjectArray = new List + { + new BarNullableDeep { Foo = new FooDeep { BarShort = 12 } } + } + } + }, + new BarNullable + { + Foo = new FooNullable + { + BarShort = null, + BarBool = null, + BarEnum = BarEnum.BAZ, + BarString = "testbtest", + ObjectArray = new List + { + new BarNullableDeep { Foo = new FooDeep { BarShort = 9 } } + } + } + }, + new BarNullable + { + Foo = new FooNullable + { + BarShort = 14, + BarBool = false, + BarEnum = BarEnum.QUX, + BarString = "testctest", + ObjectArray = new List + { + new BarNullableDeep { Foo = new FooDeep { BarShort = 14 } } + } + } + }, + new BarNullable + { + Foo = new FooNullable + { + BarShort = 13, + BarBool = false, + BarEnum = BarEnum.FOO, + BarString = "testdtest", + ObjectArray = null + } + } + }; + + private readonly SchemaCache _cache = new SchemaCache(); + + [Fact] + public async Task Create_DeepFilterObjectTwoProjections() + { + // arrange + IRequestExecutor tester = _cache.CreateSchema(_barEntities, OnModelCreating); + + // act + // assert + IExecutionResult res1 = await tester.ExecuteAsync( + QueryRequestBuilder.New() + .SetQuery( + @" + { + root { + foo { + objectArray { + foo { + barString + barShort + } + } + } + } + }") + .Create()); + + res1.MatchSqlSnapshot(); + } + + [Fact] + public async Task Create_ListObjectDifferentLevelProjection() + { + // arrange + IRequestExecutor tester = _cache.CreateSchema(_barEntities, OnModelCreating); + + // act + // assert + IExecutionResult res1 = await tester.ExecuteAsync( + QueryRequestBuilder.New() + .SetQuery( + @" + { + root { + foo { + barString + objectArray { + foo { + barString + barShort + } + } + } + } + }") + .Create()); + + res1.MatchSqlSnapshot(); + } + + [Fact] + public async Task Create_DeepFilterObjectTwoProjections_Nullable() + { + // arrange + IRequestExecutor tester = _cache.CreateSchema(_barNullableEntities, OnModelCreating); + + // act + // assert + IExecutionResult res1 = await tester.ExecuteAsync( + QueryRequestBuilder.New() + .SetQuery( + @" + { + root { + foo { + objectArray { + foo { + barString + barShort + } + } + } + } + }") + .Create()); + + res1.MatchSqlSnapshot(); + } + + [Fact] + public async Task Create_ListObjectDifferentLevelProjection_Nullable() + { + // arrange + IRequestExecutor tester = _cache.CreateSchema(_barNullableEntities, OnModelCreating); + + // act + // assert + IExecutionResult res1 = await tester.ExecuteAsync( + QueryRequestBuilder.New() + .SetQuery( + @" + { + root { + foo { + barString + objectArray { + foo { + barString + barShort + } + } + } + } + }") + .Create()); + + res1.MatchSqlSnapshot(); + } + + public static void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity().HasMany(x => x.ObjectArray); + modelBuilder.Entity().HasOne(x => x.NestedObject); + modelBuilder.Entity().HasOne(x => x.Foo); + } + + public class Foo + { + public int Id { get; set; } + + public short BarShort { get; set; } + + public string BarString { get; set; } = string.Empty; + + public BarEnum BarEnum { get; set; } + + public bool BarBool { get; set; } + + [UseFirstOrDefault] + public List ObjectArray { get; set; } + + public BarDeep NestedObject { get; set; } + } + + public class FooDeep + { + public int Id { get; set; } + + public short BarShort { get; set; } + + public string BarString { get; set; } = string.Empty; + } + + public class FooNullable + { + public int Id { get; set; } + + public short? BarShort { get; set; } + + public string? BarString { get; set; } + + public BarEnum? BarEnum { get; set; } + + public bool? BarBool { get; set; } + + [UseFirstOrDefault] + public List? ObjectArray { get; set; } + + public BarNullableDeep? NestedObject { get; set; } + } + + public class Bar + { + public int Id { get; set; } + + public Foo Foo { get; set; } + } + + public class BarDeep + { + public int Id { get; set; } + + public FooDeep Foo { get; set; } + } + + public class BarNullableDeep + { + public int Id { get; set; } + + public FooDeep? Foo { get; set; } + } + + public class BarNullable + { + public int Id { get; set; } + + public FooNullable? Foo { get; set; } + } + + public enum BarEnum + { + FOO, + BAR, + BAZ, + QUX + } + } +} diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/QueryableProjectionFilterTests.cs b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/QueryableProjectionFilterTests.cs new file mode 100644 index 00000000000..745b62506c3 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/QueryableProjectionFilterTests.cs @@ -0,0 +1,340 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using HotChocolate.Execution; +using Microsoft.EntityFrameworkCore; +using Xunit; + +namespace HotChocolate.Data.Projections.Expressions +{ + public class QueryableProjectionFilterTests + { + private static readonly Bar[] _barEntities = + { + new Bar + { + Foo = new Foo + { + BarShort = 12, + BarBool = true, + BarEnum = BarEnum.BAR, + BarString = "testatest", + NestedObject = + new BarDeep { Foo = new FooDeep { BarShort = 12, BarString = "a" } }, + ObjectArray = new List + { + new BarDeep { Foo = new FooDeep { BarShort = 12, BarString = "a" } } + } + } + }, + new Bar + { + Foo = new Foo + { + BarShort = 14, + BarBool = true, + BarEnum = BarEnum.BAZ, + BarString = "testbtest", + NestedObject = + new BarDeep { Foo = new FooDeep { BarShort = 12, BarString = "d" } }, + ObjectArray = new List + { + new BarDeep { Foo = new FooDeep { BarShort = 14, BarString = "d" } } + } + } + } + }; + + private static readonly BarNullable[] _barNullableEntities = + { + new BarNullable + { + Foo = new FooNullable + { + BarShort = 12, + BarBool = true, + BarEnum = BarEnum.BAR, + BarString = "testatest", + ObjectArray = new List + { + new BarNullableDeep { Foo = new FooDeep { BarShort = 12 } } + } + } + }, + new BarNullable + { + Foo = new FooNullable + { + BarShort = null, + BarBool = null, + BarEnum = BarEnum.BAZ, + BarString = "testbtest", + ObjectArray = new List + { + new BarNullableDeep { Foo = new FooDeep { BarShort = 9 } } + } + } + }, + new BarNullable + { + Foo = new FooNullable + { + BarShort = 14, + BarBool = false, + BarEnum = BarEnum.QUX, + BarString = "testctest", + ObjectArray = new List + { + new BarNullableDeep { Foo = new FooDeep { BarShort = 14 } } + } + } + }, + new BarNullable + { + Foo = new FooNullable + { + BarShort = 13, + BarBool = false, + BarEnum = BarEnum.FOO, + BarString = "testdtest", + ObjectArray = null + } + } + }; + + private readonly SchemaCache _cache = new SchemaCache(); + + [Fact] + public async Task Create_DeepFilterObjectTwoProjections() + { + // arrange + IRequestExecutor tester = _cache.CreateSchema(_barEntities, OnModelCreating); + + // act + // assert + IExecutionResult res1 = await tester.ExecuteAsync( + QueryRequestBuilder.New() + .SetQuery( + @" + { + root { + foo { + objectArray( + where: { + foo: { + barString: { + eq: ""a"" + } + } + }) { + foo { + barString + barShort + } + } + } + } + }") + .Create()); + + res1.MatchSqlSnapshot(); + } + + [Fact] + public async Task Create_ListObjectDifferentLevelProjection() + { + // arrange + IRequestExecutor tester = _cache.CreateSchema(_barEntities, OnModelCreating); + + // act + // assert + IExecutionResult res1 = await tester.ExecuteAsync( + QueryRequestBuilder.New() + .SetQuery( + @" + { + root { + foo { + barString + objectArray( + where: { + foo: { + barString: { + eq: ""a"" + } + } + }) { + foo { + barString + barShort + } + } + } + } + }") + .Create()); + + res1.MatchSqlSnapshot(); + } + + [Fact] + public async Task Create_DeepFilterObjectTwoProjections_Nullable() + { + // arrange + IRequestExecutor tester = _cache.CreateSchema(_barNullableEntities, OnModelCreating); + + // act + // assert + IExecutionResult res1 = await tester.ExecuteAsync( + QueryRequestBuilder.New() + .SetQuery( + @" + { + root { + foo { + objectArray( + where: { + foo: { + barString: { + eq: ""a"" + } + } + }) { + foo { + barString + barShort + } + } + } + } + }") + .Create()); + + res1.MatchSqlSnapshot(); + } + + [Fact] + public async Task Create_ListObjectDifferentLevelProjection_Nullable() + { + // arrange + IRequestExecutor tester = _cache.CreateSchema(_barNullableEntities, OnModelCreating); + + // act + // assert + IExecutionResult res1 = await tester.ExecuteAsync( + QueryRequestBuilder.New() + .SetQuery( + @" + { + root { + foo { + barString + objectArray( + where: { + foo: { + barString: { + eq: ""a"" + } + } + }) { + foo { + barString + barShort + } + } + } + } + }") + .Create()); + + res1.MatchSqlSnapshot(); + } + + public static void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity().HasMany(x => x.ObjectArray); + modelBuilder.Entity().HasOne(x => x.NestedObject); + modelBuilder.Entity().HasOne(x => x.Foo); + } + + public class Foo + { + public int Id { get; set; } + + public short BarShort { get; set; } + + public string BarString { get; set; } = string.Empty; + + public BarEnum BarEnum { get; set; } + + public bool BarBool { get; set; } + + [UseFiltering] + public List ObjectArray { get; set; } + + public BarDeep NestedObject { get; set; } + } + + public class FooDeep + { + public int Id { get; set; } + + public short BarShort { get; set; } + + public string BarString { get; set; } = string.Empty; + } + + public class FooNullable + { + public int Id { get; set; } + + public short? BarShort { get; set; } + + public string? BarString { get; set; } + + public BarEnum? BarEnum { get; set; } + + public bool? BarBool { get; set; } + + [UseFiltering] + public List? ObjectArray { get; set; } + + public BarNullableDeep? NestedObject { get; set; } + } + + public class Bar + { + public int Id { get; set; } + + public Foo Foo { get; set; } + } + + public class BarDeep + { + public int Id { get; set; } + + public FooDeep Foo { get; set; } + } + + public class BarNullableDeep + { + public int Id { get; set; } + + public FooDeep? Foo { get; set; } + } + + public class BarNullable + { + public int Id { get; set; } + + public FooNullable? Foo { get; set; } + } + + public enum BarEnum + { + FOO, + BAR, + BAZ, + QUX + } + } +} diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/QueryableProjectionHashSetTest.cs b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/QueryableProjectionHashSetTest.cs new file mode 100644 index 00000000000..2c11b4881e6 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/QueryableProjectionHashSetTest.cs @@ -0,0 +1,196 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using HotChocolate.Execution; +using Microsoft.EntityFrameworkCore; +using Xunit; + +namespace HotChocolate.Data.Projections.Expressions +{ + public class QueryableProjectionHashSetTests + { + private static readonly Bar[] _barEntities = + { + new Bar + { + Foo = new Foo + { + BarShort = 12, + BarBool = true, + BarEnum = BarEnum.BAR, + BarString = "testatest", + NestedObject = + new BarDeep { Foo = new FooDeep { BarShort = 12, BarString = "a" } }, + ObjectSet = new HashSet + { + new BarDeep { Foo = new FooDeep { BarShort = 12, BarString = "a" } } + } + } + }, + new Bar + { + Foo = new Foo + { + BarShort = 14, + BarBool = true, + BarEnum = BarEnum.BAZ, + BarString = "testbtest", + NestedObject = + new BarDeep { Foo = new FooDeep { BarShort = 12, BarString = "d" } }, + ObjectSet = new HashSet + { + new BarDeep { Foo = new FooDeep { BarShort = 14, BarString = "d" } } + } + } + } + }; + + private readonly SchemaCache _cache = new SchemaCache(); + + [Fact] + public async Task Create_DeepFilterObjectTwoProjections() + { + // arrange + IRequestExecutor tester = _cache.CreateSchema(_barEntities, OnModelCreating); + + // act + // assert + IExecutionResult res1 = await tester.ExecuteAsync( + QueryRequestBuilder.New() + .SetQuery( + @" + { + root { + foo { + objectSet { + foo { + barString + barShort + } + } + } + } + }") + .Create()); + + res1.MatchSqlSnapshot(); + } + + [Fact] + public async Task Create_ListObjectDifferentLevelProjection() + { + // arrange + IRequestExecutor tester = _cache.CreateSchema(_barEntities, OnModelCreating); + + // act + // assert + IExecutionResult res1 = await tester.ExecuteAsync( + QueryRequestBuilder.New() + .SetQuery( + @" + { + root { + foo { + barString + objectSet { + foo { + barString + barShort + } + } + } + } + }") + .Create()); + + res1.MatchSqlSnapshot(); + } + + public static void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity().HasMany(x => x.ObjectSet); + modelBuilder.Entity().HasOne(x => x.NestedObject); + modelBuilder.Entity().HasOne(x => x.Foo); + } + + public class Foo + { + public int Id { get; set; } + + public short BarShort { get; set; } + + public string BarString { get; set; } = string.Empty; + + public BarEnum BarEnum { get; set; } + + public bool BarBool { get; set; } + + [UseFiltering] + public HashSet ObjectSet { get; set; } + + public BarDeep NestedObject { get; set; } + } + + public class FooDeep + { + public int Id { get; set; } + + public short BarShort { get; set; } + + public string BarString { get; set; } = string.Empty; + } + + public class FooNullable + { + public int Id { get; set; } + + public short? BarShort { get; set; } + + public string? BarString { get; set; } + + public BarEnum? BarEnum { get; set; } + + public bool? BarBool { get; set; } + + [UseFiltering] + public HashSet? ObjectSet { get; set; } + + public BarNullableDeep? NestedObject { get; set; } + } + + public class Bar + { + public int Id { get; set; } + + public Foo Foo { get; set; } + } + + public class BarDeep + { + public int Id { get; set; } + + public FooDeep Foo { get; set; } + } + + public class BarNullableDeep + { + public int Id { get; set; } + + public FooDeep? Foo { get; set; } + } + + public class BarNullable + { + public int Id { get; set; } + + public FooNullable? Foo { get; set; } + } + + public enum BarEnum + { + FOO, + BAR, + BAZ, + QUX + } + } +} diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/QueryableProjectionSetTest.cs b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/QueryableProjectionSetTest.cs new file mode 100644 index 00000000000..b268f39c80e --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/QueryableProjectionSetTest.cs @@ -0,0 +1,196 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using HotChocolate.Execution; +using Microsoft.EntityFrameworkCore; +using Xunit; + +namespace HotChocolate.Data.Projections.Expressions +{ + public class QueryableProjectionISetTests + { + private static readonly Bar[] _barEntities = + { + new Bar + { + Foo = new Foo + { + BarShort = 12, + BarBool = true, + BarEnum = BarEnum.BAR, + BarString = "testatest", + NestedObject = + new BarDeep { Foo = new FooDeep { BarShort = 12, BarString = "a" } }, + ObjectSet = new HashSet + { + new BarDeep { Foo = new FooDeep { BarShort = 12, BarString = "a" } } + } + } + }, + new Bar + { + Foo = new Foo + { + BarShort = 14, + BarBool = true, + BarEnum = BarEnum.BAZ, + BarString = "testbtest", + NestedObject = + new BarDeep { Foo = new FooDeep { BarShort = 12, BarString = "d" } }, + ObjectSet = new HashSet + { + new BarDeep { Foo = new FooDeep { BarShort = 14, BarString = "d" } } + } + } + } + }; + + private readonly SchemaCache _cache = new SchemaCache(); + + [Fact] + public async Task Create_DeepFilterObjectTwoProjections() + { + // arrange + IRequestExecutor tester = _cache.CreateSchema(_barEntities, OnModelCreating); + + // act + // assert + IExecutionResult res1 = await tester.ExecuteAsync( + QueryRequestBuilder.New() + .SetQuery( + @" + { + root { + foo { + objectSet { + foo { + barString + barShort + } + } + } + } + }") + .Create()); + + res1.MatchSqlSnapshot(); + } + + [Fact] + public async Task Create_ListObjectDifferentLevelProjection() + { + // arrange + IRequestExecutor tester = _cache.CreateSchema(_barEntities, OnModelCreating); + + // act + // assert + IExecutionResult res1 = await tester.ExecuteAsync( + QueryRequestBuilder.New() + .SetQuery( + @" + { + root { + foo { + barString + objectSet { + foo { + barString + barShort + } + } + } + } + }") + .Create()); + + res1.MatchSqlSnapshot(); + } + + public static void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity().HasMany(x => x.ObjectSet); + modelBuilder.Entity().HasOne(x => x.NestedObject); + modelBuilder.Entity().HasOne(x => x.Foo); + } + + public class Foo + { + public int Id { get; set; } + + public short BarShort { get; set; } + + public string BarString { get; set; } = string.Empty; + + public BarEnum BarEnum { get; set; } + + public bool BarBool { get; set; } + + [UseFiltering] + public ISet ObjectSet { get; set; } + + public BarDeep NestedObject { get; set; } + } + + public class FooDeep + { + public int Id { get; set; } + + public short BarShort { get; set; } + + public string BarString { get; set; } = string.Empty; + } + + public class FooNullable + { + public int Id { get; set; } + + public short? BarShort { get; set; } + + public string? BarString { get; set; } + + public BarEnum? BarEnum { get; set; } + + public bool? BarBool { get; set; } + + [UseFiltering] + public ISet? ObjectSet { get; set; } + + public BarNullableDeep? NestedObject { get; set; } + } + + public class Bar + { + public int Id { get; set; } + + public Foo Foo { get; set; } + } + + public class BarDeep + { + public int Id { get; set; } + + public FooDeep Foo { get; set; } + } + + public class BarNullableDeep + { + public int Id { get; set; } + + public FooDeep? Foo { get; set; } + } + + public class BarNullable + { + public int Id { get; set; } + + public FooNullable? Foo { get; set; } + } + + public enum BarEnum + { + FOO, + BAR, + BAZ, + QUX + } + } +} diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/QueryableProjectionSortedSetTest.cs b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/QueryableProjectionSortedSetTest.cs new file mode 100644 index 00000000000..f2302a4b6b5 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/QueryableProjectionSortedSetTest.cs @@ -0,0 +1,196 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using HotChocolate.Execution; +using Microsoft.EntityFrameworkCore; +using Xunit; + +namespace HotChocolate.Data.Projections.Expressions +{ + public class QueryableProjectionSortedSetTests + { + private static readonly Bar[] _barEntities = + { + new Bar + { + Foo = new Foo + { + BarShort = 12, + BarBool = true, + BarEnum = BarEnum.BAR, + BarString = "testatest", + NestedObject = + new BarDeep { Foo = new FooDeep { BarShort = 12, BarString = "a" } }, + ObjectSet = new SortedSet + { + new BarDeep { Foo = new FooDeep { BarShort = 12, BarString = "a" } } + } + } + }, + new Bar + { + Foo = new Foo + { + BarShort = 14, + BarBool = true, + BarEnum = BarEnum.BAZ, + BarString = "testbtest", + NestedObject = + new BarDeep { Foo = new FooDeep { BarShort = 12, BarString = "d" } }, + ObjectSet = new SortedSet + { + new BarDeep { Foo = new FooDeep { BarShort = 14, BarString = "d" } } + } + } + } + }; + + private readonly SchemaCache _cache = new SchemaCache(); + + [Fact] + public async Task Create_DeepFilterObjectTwoProjections() + { + // arrange + IRequestExecutor tester = _cache.CreateSchema(_barEntities, OnModelCreating); + + // act + // assert + IExecutionResult res1 = await tester.ExecuteAsync( + QueryRequestBuilder.New() + .SetQuery( + @" + { + root { + foo { + objectSet { + foo { + barString + barShort + } + } + } + } + }") + .Create()); + + res1.MatchSqlSnapshot(); + } + + [Fact] + public async Task Create_ListObjectDifferentLevelProjection() + { + // arrange + IRequestExecutor tester = _cache.CreateSchema(_barEntities, OnModelCreating); + + // act + // assert + IExecutionResult res1 = await tester.ExecuteAsync( + QueryRequestBuilder.New() + .SetQuery( + @" + { + root { + foo { + barString + objectSet { + foo { + barString + barShort + } + } + } + } + }") + .Create()); + + res1.MatchSqlSnapshot(); + } + + public static void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity().HasMany(x => x.ObjectSet); + modelBuilder.Entity().HasOne(x => x.NestedObject); + modelBuilder.Entity().HasOne(x => x.Foo); + } + + public class Foo + { + public int Id { get; set; } + + public short BarShort { get; set; } + + public string BarString { get; set; } = string.Empty; + + public BarEnum BarEnum { get; set; } + + public bool BarBool { get; set; } + + [UseFiltering] + public SortedSet ObjectSet { get; set; } + + public BarDeep NestedObject { get; set; } + } + + public class FooDeep + { + public int Id { get; set; } + + public short BarShort { get; set; } + + public string BarString { get; set; } = string.Empty; + } + + public class FooNullable + { + public int Id { get; set; } + + public short? BarShort { get; set; } + + public string? BarString { get; set; } + + public BarEnum? BarEnum { get; set; } + + public bool? BarBool { get; set; } + + [UseFiltering] + public SortedSet? ObjectSet { get; set; } + + public BarNullableDeep? NestedObject { get; set; } + } + + public class Bar + { + public int Id { get; set; } + + public Foo Foo { get; set; } + } + + public class BarDeep + { + public int Id { get; set; } + + public FooDeep Foo { get; set; } + } + + public class BarNullableDeep + { + public int Id { get; set; } + + public FooDeep? Foo { get; set; } + } + + public class BarNullable + { + public int Id { get; set; } + + public FooNullable? Foo { get; set; } + } + + public enum BarEnum + { + FOO, + BAR, + BAZ, + QUX + } + } +} diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/QueryableProjectionSortingTests.cs b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/QueryableProjectionSortingTests.cs new file mode 100644 index 00000000000..51d4aad0604 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/QueryableProjectionSortingTests.cs @@ -0,0 +1,404 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using HotChocolate.Execution; +using Microsoft.EntityFrameworkCore; +using Xunit; + +namespace HotChocolate.Data.Projections.Expressions +{ + public class QueryableProjectionSortingTests + { + private static readonly Bar[] _barEntities = + { + new Bar + { + Foo = new Foo + { + BarShort = 12, + BarBool = true, + BarEnum = BarEnum.BAR, + BarString = "testatest", + NestedObject = + new BarDeep { Foo = new FooDeep { BarShort = 12, BarString = "a" } }, + ObjectArray = new List + { + new BarDeep { Foo = new FooDeep { BarShort = 1, BarString = "a" } }, + new BarDeep { Foo = new FooDeep { BarShort = 12, BarString = "a" } }, + new BarDeep { Foo = new FooDeep { BarShort = 3, BarString = "a" } } + } + } + }, + new Bar + { + Foo = new Foo + { + BarShort = 14, + BarBool = true, + BarEnum = BarEnum.BAZ, + BarString = "testbtest", + NestedObject = + new BarDeep { Foo = new FooDeep { BarShort = 12, BarString = "d" } }, + ObjectArray = new List + { + new BarDeep { Foo = new FooDeep { BarShort = 1, BarString = "a" } }, + new BarDeep { Foo = new FooDeep { BarShort = 12, BarString = "a" } }, + new BarDeep { Foo = new FooDeep { BarShort = 3, BarString = "a" } } + } + } + } + }; + + private static readonly BarNullable[] _barNullableEntities = + { + new BarNullable + { + Foo = new FooNullable + { + BarShort = 12, + BarBool = true, + BarEnum = BarEnum.BAR, + BarString = "testatest", + NestedObject = + new BarDeepNullable + { + Foo = new FooDeep { BarShort = 12, BarString = "a" } + }, + ObjectArray = new List + { + new BarDeepNullable + { + Foo = new FooDeep { BarShort = 1, BarString = "a" } + }, + new BarDeepNullable + { + Foo = new FooDeep { BarShort = 12, BarString = "a" } + }, + new BarDeepNullable + { + Foo = new FooDeep { BarShort = 3, BarString = "a" } + } + } + } + }, + new BarNullable + { + Foo = new FooNullable + { + BarShort = null, + BarBool = null, + BarEnum = BarEnum.BAZ, + BarString = "testbtest", + NestedObject = + new BarDeepNullable + { + Foo = new FooDeep { BarShort = 12, BarString = "a" } + }, + ObjectArray = new List + { + new BarDeepNullable + { + Foo = new FooDeep { BarShort = 1, BarString = "a" } + }, + new BarDeepNullable + { + Foo = new FooDeep { BarShort = 12, BarString = "a" } + }, + new BarDeepNullable + { + Foo = new FooDeep { BarShort = 3, BarString = "a" } + } + } + } + }, + new BarNullable + { + Foo = new FooNullable + { + BarShort = 14, + BarBool = false, + BarEnum = BarEnum.QUX, + BarString = "testctest", + NestedObject = + new BarDeepNullable + { + Foo = new FooDeep { BarShort = 12, BarString = "a" } + }, + ObjectArray = new List + { + new BarDeepNullable + { + Foo = new FooDeep { BarShort = 1, BarString = "a" } + }, + new BarDeepNullable + { + Foo = new FooDeep { BarShort = 12, BarString = "a" } + }, + new BarDeepNullable + { + Foo = new FooDeep { BarShort = 3, BarString = "a" } + } + } + } + }, + new BarNullable + { + Foo = new FooNullable + { + BarShort = 13, + BarBool = false, + BarEnum = BarEnum.FOO, + BarString = "testdtest", + ObjectArray = null + } + } + }; + + private readonly SchemaCache _cache = new SchemaCache(); + + [Fact] + public async Task Create_DeepFilterObjectTwoProjections() + { + // arrange + IRequestExecutor tester = _cache.CreateSchema(_barEntities, OnModelCreating); + + // act + // assert + IExecutionResult res1 = await tester.ExecuteAsync( + QueryRequestBuilder.New() + .SetQuery( + @" + { + root { + foo { + objectArray( + order: { + foo: { + barShort: ASC + } + }) { + foo { + barString + barShort + } + } + } + } + }") + .Create()); + + res1.MatchSqlSnapshot(); + } + + [Fact] + public async Task Create_ListObjectDifferentLevelProjection() + { + // arrange + IRequestExecutor tester = _cache.CreateSchema(_barEntities, OnModelCreating); + + // act + // assert + IExecutionResult res1 = await tester.ExecuteAsync( + QueryRequestBuilder.New() + .SetQuery( + @" + { + root { + foo { + barString + objectArray( + order: { + foo: { + barShort: ASC + } + }) { + foo { + barString + barShort + } + } + } + } + }") + .Create()); + + res1.MatchSqlSnapshot(); + } + + [Fact] + public async Task Create_DeepFilterObjectTwoProjections_Nullable() + { + // arrange + IRequestExecutor tester = _cache.CreateSchema( + _barNullableEntities, + OnModelCreatingNullable); + + // act + // assert + IExecutionResult res1 = await tester.ExecuteAsync( + QueryRequestBuilder.New() + .SetQuery( + @" + { + root { + foo { + objectArray( + order: { + foo: { + barShort: ASC + } + }) { + foo { + barString + barShort + } + } + } + } + }") + .Create()); + + res1.MatchSqlSnapshot(); + } + + [Fact] + public async Task Create_ListObjectDifferentLevelProjection_Nullable() + { + // arrange + IRequestExecutor tester = _cache.CreateSchema( + _barNullableEntities, + OnModelCreatingNullable); + + // act + // assert + IExecutionResult res1 = await tester.ExecuteAsync( + QueryRequestBuilder.New() + .SetQuery( + @" + { + root { + foo { + barString + objectArray( + order: { + foo: { + barShort: ASC + } + }) { + foo { + barString + barShort + } + } + } + } + }") + .Create()); + + res1.MatchSqlSnapshot(); + } + + public static void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity().HasMany(x => x.ObjectArray); + modelBuilder.Entity().HasOne(x => x.NestedObject); + modelBuilder.Entity().HasOne(x => x.Foo); + } + + public static void OnModelCreatingNullable(ModelBuilder modelBuilder) + { + modelBuilder.Entity().HasMany(x => x.ObjectArray); + modelBuilder.Entity().HasOne(x => x.NestedObject); + modelBuilder.Entity().HasOne(x => x.Foo); + modelBuilder.Entity(); + } + + public class Foo + { + public int Id { get; set; } + + public short BarShort { get; set; } + + public string BarString { get; set; } = string.Empty; + + public BarEnum BarEnum { get; set; } + + public bool BarBool { get; set; } + + [UseSorting] + public List ObjectArray { get; set; } + + public BarDeep NestedObject { get; set; } + } + + public class FooDeep + { + public int Id { get; set; } + + public short BarShort { get; set; } + + public string BarString { get; set; } = string.Empty; + } + + public class FooNullable + { + public int Id { get; set; } + + public short? BarShort { get; set; } + + public string? BarString { get; set; } + + public BarEnum? BarEnum { get; set; } + + public bool? BarBool { get; set; } + + public List? ObjectArray { get; set; } + + public BarDeepNullable? NestedObject { get; set; } + } + + public class Bar + { + public int Id { get; set; } + + public Foo Foo { get; set; } + } + + public class BarDeep + { + public int Id { get; set; } + + public FooDeep Foo { get; set; } + } + + public class BarDeepNullable + { + public int Id { get; set; } + + public FooDeep Foo { get; set; } + } + + public class BarNullable + { + public int Id { get; set; } + + public FooNullable? Foo { get; set; } + } + + public class FooDeepNullable + { + public int Id { get; set; } + + public short BarShort { get; set; } + + public string BarString { get; set; } = string.Empty; + } + + public enum BarEnum + { + FOO, + BAR, + BAZ, + QUX + } + } +} diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/QueryableProjectionVisitorIsProjectedTests.cs b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/QueryableProjectionVisitorIsProjectedTests.cs new file mode 100644 index 00000000000..7e879883d9d --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/QueryableProjectionVisitorIsProjectedTests.cs @@ -0,0 +1,109 @@ +using System.Threading.Tasks; +using HotChocolate.Execution; +using Xunit; + +namespace HotChocolate.Data.Projections +{ + public class QueryableProjectionVisitorIsProjectedTests + { + private static readonly Foo[] _fooEntities = + { + new Foo { IsProjectedTrue = true, IsProjectedFalse = false }, + new Foo { IsProjectedTrue = true, IsProjectedFalse = false } + }; + + private static readonly Bar[] _barEntities = + { + new Bar { IsProjectedFalse = false }, new Bar { IsProjectedFalse = false } + }; + + private readonly SchemaCache _cache = new SchemaCache(); + + [Fact] + public async Task IsProjected_Should_NotBeProjectedWhenSelected_When_FalseWithOneProps() + { + // arrange + IRequestExecutor tester = _cache.CreateSchema(_fooEntities); + + // act + // assert + IExecutionResult res1 = await tester.ExecuteAsync( + QueryRequestBuilder.New() + .SetQuery("{ root { isProjectedFalse }}") + .Create()); + + res1.MatchSqlSnapshot(); + } + + [Fact] + public async Task IsProjected_Should_NotBeProjectedWhenSelected_When_FalseWithTwoProps() + { + // arrange + IRequestExecutor tester = _cache.CreateSchema(_fooEntities); + + // act + // assert + IExecutionResult res1 = await tester.ExecuteAsync( + QueryRequestBuilder.New() + .SetQuery("{ root { isProjectedFalse isProjectedTrue }}") + .Create()); + + res1.MatchSqlSnapshot(); + } + + [Fact] + public async Task IsProjected_Should_AlwaysBeProjectedWhenSelected_When_True() + { + // arrange + IRequestExecutor tester = _cache.CreateSchema(_fooEntities); + + // act + // assert + IExecutionResult res1 = await tester.ExecuteAsync( + QueryRequestBuilder.New() + .SetQuery("{ root { isProjectedFalse }}") + .Create()); + + res1.MatchSqlSnapshot(); + } + + [Fact] + public async Task IsProjected_Should_NotFailWhenSelectionSetSkippedCompletely() + { + // arrange + IRequestExecutor tester = _cache.CreateSchema(_barEntities); + + // act + // assert + IExecutionResult res1 = await tester.ExecuteAsync( + QueryRequestBuilder.New() + .SetQuery("{ root { isProjectedFalse }}") + .Create()); + + res1.MatchSqlSnapshot(); + } + + public class Foo + { + public int Id { get; set; } + + [IsProjected(true)] + public bool? IsProjectedTrue { get; set; } + + [IsProjected(false)] + public bool? IsProjectedFalse { get; set; } + + public bool? ShouldNeverBeProjected { get; set; } + } + + public class Bar + { + public int Id { get; set; } + + [IsProjected(false)] + public bool? IsProjectedFalse { get; set; } + + public bool? ShouldNeverBeProjected { get; set; } + } + } +} diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/QueryableProjectionVisitorPagingTests.css.cs b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/QueryableProjectionVisitorPagingTests.css.cs new file mode 100644 index 00000000000..2cfb107abf0 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/QueryableProjectionVisitorPagingTests.css.cs @@ -0,0 +1,333 @@ +using System.Collections.Generic; +using System.Reflection; +using System.Threading.Tasks; +using HotChocolate.Execution; +using HotChocolate.Types; +using HotChocolate.Types.Descriptors; +using Xunit; + +namespace HotChocolate.Data.Projections +{ + public class QueryableProjectionVisitorPagingTests + { + private static readonly Foo[] _fooEntities = + { + new Foo { Bar = true, Baz = "a" }, new Foo { Bar = false, Baz = "b" } + }; + + private static readonly FooNullable[] _fooNullableEntities = + { + new FooNullable { Bar = true, Baz = "a" }, + new FooNullable { Bar = null, Baz = null }, + new FooNullable { Bar = false, Baz = "c" } + }; + + private readonly SchemaCache _cache = new SchemaCache(); + + [Fact] + public async Task Create_ProjectsTwoProperties_Nodes() + { + // arrange + IRequestExecutor tester = _cache.CreateSchema( + _fooEntities, + usePaging: true); + + // act + // assert + IExecutionResult res1 = await tester.ExecuteAsync( + QueryRequestBuilder.New() + .SetQuery("{ root{ nodes { bar baz } }}") + .Create()); + + res1.MatchSqlSnapshot(); + } + + [Fact] + public async Task Create_ProjectsOneProperty_Nodes() + { + // arrange + IRequestExecutor tester = _cache.CreateSchema( + _fooEntities, + usePaging: true); + + // act + // assert + IExecutionResult res1 = await tester.ExecuteAsync( + QueryRequestBuilder.New() + .SetQuery("{ root{ nodes { baz } }}") + .Create()); + + res1.MatchSqlSnapshot(); + } + + [Fact] + public async Task Create_ProjectsTwoProperties_Edges() + { + // arrange + IRequestExecutor tester = _cache.CreateSchema( + _fooEntities, + usePaging: true); + + // act + // assert + IExecutionResult res1 = await tester.ExecuteAsync( + QueryRequestBuilder.New() + .SetQuery("{ root{ edges { node { bar baz }} }}") + .Create()); + + res1.MatchSqlSnapshot(); + } + + [Fact] + public async Task Create_ProjectsOneProperty_Edges() + { + // arrange + IRequestExecutor tester = _cache.CreateSchema( + _fooEntities, + usePaging: true); + + // act + // assert + IExecutionResult res1 = await tester.ExecuteAsync( + QueryRequestBuilder.New() + .SetQuery("{ root{ edges { node { baz }} }}") + .Create()); + + res1.MatchSqlSnapshot(); + } + + [Fact] + public async Task Create_ProjectsTwoProperties_EdgesAndNodes() + { + // arrange + IRequestExecutor tester = _cache.CreateSchema( + _fooEntities, + usePaging: true); + + // act + // assert + IExecutionResult res1 = await tester.ExecuteAsync( + QueryRequestBuilder.New() + .SetQuery("{ root{ nodes{ baz } edges { node { bar }} }}") + .Create()); + + res1.MatchSqlSnapshot(); + } + + [Fact] + public async Task Create_ProjectsOneProperty_EdgesAndNodesOverlap() + { + // arrange + IRequestExecutor tester = _cache.CreateSchema( + _fooEntities, + usePaging: true); + + // act + // assert + IExecutionResult res1 = await tester.ExecuteAsync( + QueryRequestBuilder.New() + .SetQuery("{ root{ nodes{ baz } edges { node { baz }} }}") + .Create()); + + res1.MatchSqlSnapshot(); + } + + [Fact] + public async Task CreateNullable_ProjectsTwoProperties_Nodes() + { + // arrange + IRequestExecutor tester = _cache.CreateSchema( + _fooNullableEntities, + usePaging: true); + + // act + // assert + IExecutionResult res1 = await tester.ExecuteAsync( + QueryRequestBuilder.New() + .SetQuery("{ root{ nodes { bar baz } }}") + .Create()); + + res1.MatchSqlSnapshot(); + } + + [Fact] + public async Task CreateNullable_ProjectsOneProperty_Nodes() + { + // arrange + IRequestExecutor tester = _cache.CreateSchema( + _fooNullableEntities, + usePaging: true); + + // act + // assert + IExecutionResult res1 = await tester.ExecuteAsync( + QueryRequestBuilder.New() + .SetQuery("{ root{ nodes { baz } }}") + .Create()); + + res1.MatchSqlSnapshot(); + } + + [Fact] + public async Task CreateNullable_ProjectsTwoProperties_Edges() + { + // arrange + IRequestExecutor tester = _cache.CreateSchema( + _fooNullableEntities, + usePaging: true); + + // act + // assert + IExecutionResult res1 = await tester.ExecuteAsync( + QueryRequestBuilder.New() + .SetQuery("{ root{ edges { node { bar baz }} }}") + .Create()); + + res1.MatchSqlSnapshot(); + } + + [Fact] + public async Task CreateNullable_ProjectsOneProperty_Edges() + { + // arrange + IRequestExecutor tester = _cache.CreateSchema( + _fooNullableEntities, + usePaging: true); + + // act + // assert + IExecutionResult res1 = await tester.ExecuteAsync( + QueryRequestBuilder.New() + .SetQuery("{ root{ edges { node { baz }} }}") + .Create()); + + res1.MatchSqlSnapshot(); + } + + [Fact] + public async Task CreateNullable_ProjectsTwoProperties_EdgesAndNodes() + { + // arrange + IRequestExecutor tester = _cache.CreateSchema( + _fooNullableEntities, + usePaging: true); + + // act + // assert + IExecutionResult res1 = await tester.ExecuteAsync( + QueryRequestBuilder.New() + .SetQuery("{ root{ nodes{ baz } edges { node { bar }} }}") + .Create()); + + res1.MatchSqlSnapshot(); + } + + [Fact] + public async Task CreateNullable_ProjectsOneProperty_EdgesAndNodesOverlap() + { + // arrange + IRequestExecutor tester = _cache.CreateSchema( + _fooNullableEntities, + usePaging: true); + + // act + // assert + IExecutionResult res1 = await tester.ExecuteAsync( + QueryRequestBuilder.New() + .SetQuery("{ root{ nodes{ baz } edges { node { baz }} }}") + .Create()); + + res1.MatchSqlSnapshot(); + } + + [Fact] + public async Task Create_Projection_Should_Stop_When_UseProjectionEncountered() + { + // arrange + IRequestExecutor tester = _cache.CreateSchema( + _fooEntities, + usePaging: true); + + // act + // assert + IExecutionResult res1 = await tester.ExecuteAsync( + QueryRequestBuilder.New() + .SetQuery("{ root{ nodes{ bar list { barBaz } } }}") + .Create()); + + res1.MatchSqlSnapshot(); + } + + [Fact] + public async Task Create_Projection_Should_Stop_When_UsePagingEncountered() + { + // arrange + IRequestExecutor tester = _cache.CreateSchema( + _fooEntities, + usePaging: true); + + // act + // assert + IExecutionResult res1 = await tester.ExecuteAsync( + QueryRequestBuilder.New() + .SetQuery("{ root{ nodes{ bar paging { nodes {barBaz }} } }}") + .Create()); + + res1.MatchSqlSnapshot(); + } + + public class Foo + { + public int Id { get; set; } + + public bool Bar { get; set; } + + public string Baz { get; set; } + + public string? Qux { get; set; } + + [UseProjection] + [SubData] + public List List { get; set; } + + [UsePaging] + [UseProjection] + [SubData] + public List Paging { get; set; } + } + + public class Bar + { + public int Id { get; set; } + + public string? BarBaz { get; set; } + + public string? BarQux { get; set; } + } + + public class FooNullable + { + public int Id { get; set; } + + public bool? Bar { get; set; } + + public string? Baz { get; set; } + } + + public class SubDataAttribute : ObjectFieldDescriptorAttribute + { + public override void OnConfigure( + IDescriptorContext context, + IObjectFieldDescriptor descriptor, + MemberInfo member) + { + descriptor.Resolver( + new List + { + new Bar { BarBaz = "a_a", BarQux = "a_c" }, + new Bar { BarBaz = "a_b", BarQux = "a_d" } + }); + } + } + } +} diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/QueryableProjectionVisitorScalarTests.cs b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/QueryableProjectionVisitorScalarTests.cs new file mode 100644 index 00000000000..886ba1de6ab --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/QueryableProjectionVisitorScalarTests.cs @@ -0,0 +1,89 @@ +using System.Threading.Tasks; +using HotChocolate.Execution; +using HotChocolate.Types; +using Xunit; + +namespace HotChocolate.Data.Projections +{ + public class QueryableProjectionVisitorScalarTests + { + private static readonly Foo[] _fooEntities = + { + new Foo { Bar = true, Baz = "a" }, new Foo { Bar = false, Baz = "b" } + }; + + private readonly SchemaCache _cache = new SchemaCache(); + + [Fact] + public async Task Create_ProjectsTwoProperties_Expression() + { + // arrange + IRequestExecutor tester = _cache.CreateSchema(_fooEntities); + + // act + // assert + IExecutionResult res1 = await tester.ExecuteAsync( + QueryRequestBuilder.New() + .SetQuery("{ root{ bar baz }}") + .Create()); + + res1.MatchSqlSnapshot(); + } + + [Fact] + public async Task Create_ProjectsOneProperty_Expression() + { + // arrange + IRequestExecutor tester = _cache.CreateSchema(_fooEntities); + + // act + // assert + IExecutionResult res1 = await tester.ExecuteAsync( + QueryRequestBuilder.New() + .SetQuery("{ root{ baz }}") + .Create()); + + res1.MatchSqlSnapshot(); + } + + [Fact] + public async Task Create_ProjectsOneProperty_WithResolver() + { + // arrange + IRequestExecutor tester = _cache.CreateSchema( + _fooEntities, + objectType: new ObjectType( + x => x + .Field("foo") + .Resolver(new[] { "foo" }) + .Type>())); + + // act + // assert + IExecutionResult res1 = await tester.ExecuteAsync( + QueryRequestBuilder.New() + .SetQuery("{ root{ baz foo }}") + .Create()); + + res1.MatchSqlSnapshot(); + } + + public class Foo + { + public int Id { get; set; } + + public bool Bar { get; set; } + + public string Baz { get; set; } + } + + public class FooNullable + { + public int Id { get; set; } + + public bool? Bar { get; set; } + + public string? Baz { get; set; } + } + } +} diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/QueryableSingleOrDefaultTests.cs b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/QueryableSingleOrDefaultTests.cs new file mode 100644 index 00000000000..9fc32b2de61 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/QueryableSingleOrDefaultTests.cs @@ -0,0 +1,312 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using HotChocolate.Execution; +using Microsoft.EntityFrameworkCore; +using Xunit; + +namespace HotChocolate.Data.Projections.Expressions +{ + public class QueryableSingleOrDefaultTests + { + private static readonly Bar[] _barEntities = + { + new Bar + { + Foo = new Foo + { + BarShort = 12, + BarBool = true, + BarEnum = BarEnum.BAR, + BarString = "testatest", + NestedObject = + new BarDeep { Foo = new FooDeep { BarShort = 12, BarString = "a" } }, + ObjectArray = new List + { + new BarDeep { Foo = new FooDeep { BarShort = 12, BarString = "a" } } + } + } + }, + new Bar + { + Foo = new Foo + { + BarShort = 14, + BarBool = true, + BarEnum = BarEnum.BAZ, + BarString = "testbtest", + NestedObject = + new BarDeep { Foo = new FooDeep { BarShort = 12, BarString = "d" } }, + ObjectArray = new List + { + new BarDeep { Foo = new FooDeep { BarShort = 14, BarString = "d" } } + } + } + } + }; + + private static readonly BarNullable[] _barNullableEntities = + { + new BarNullable + { + Foo = new FooNullable + { + BarShort = 12, + BarBool = true, + BarEnum = BarEnum.BAR, + BarString = "testatest", + ObjectArray = new List + { + new BarNullableDeep { Foo = new FooDeep { BarShort = 12 } } + } + } + }, + new BarNullable + { + Foo = new FooNullable + { + BarShort = null, + BarBool = null, + BarEnum = BarEnum.BAZ, + BarString = "testbtest", + ObjectArray = new List + { + new BarNullableDeep { Foo = new FooDeep { BarShort = 9 } } + } + } + }, + new BarNullable + { + Foo = new FooNullable + { + BarShort = 14, + BarBool = false, + BarEnum = BarEnum.QUX, + BarString = "testctest", + ObjectArray = new List + { + new BarNullableDeep { Foo = new FooDeep { BarShort = 14 } } + } + } + }, + new BarNullable + { + Foo = new FooNullable + { + BarShort = 13, + BarBool = false, + BarEnum = BarEnum.FOO, + BarString = "testdtest", + ObjectArray = null + } + } + }; + + private readonly SchemaCache _cache = new SchemaCache(); + + [Fact] + public async Task Create_DeepFilterObjectTwoProjections() + { + // arrange + IRequestExecutor tester = _cache.CreateSchema(_barEntities, OnModelCreating); + + // act + // assert + IExecutionResult res1 = await tester.ExecuteAsync( + QueryRequestBuilder.New() + .SetQuery( + @" + { + root { + foo { + objectArray { + foo { + barString + barShort + } + } + } + } + }") + .Create()); + + res1.MatchSqlSnapshot(); + } + + [Fact] + public async Task Create_ListObjectDifferentLevelProjection() + { + // arrange + IRequestExecutor tester = _cache.CreateSchema(_barEntities, OnModelCreating); + + // act + // assert + IExecutionResult res1 = await tester.ExecuteAsync( + QueryRequestBuilder.New() + .SetQuery( + @" + { + root { + foo { + barString + objectArray { + foo { + barString + barShort + } + } + } + } + }") + .Create()); + + res1.MatchSqlSnapshot(); + } + + [Fact] + public async Task Create_DeepFilterObjectTwoProjections_Nullable() + { + // arrange + IRequestExecutor tester = _cache.CreateSchema(_barNullableEntities, OnModelCreating); + + // act + // assert + IExecutionResult res1 = await tester.ExecuteAsync( + QueryRequestBuilder.New() + .SetQuery( + @" + { + root { + foo { + objectArray { + foo { + barString + barShort + } + } + } + } + }") + .Create()); + + res1.MatchSqlSnapshot(); + } + + [Fact] + public async Task Create_ListObjectDifferentLevelProjection_Nullable() + { + // arrange + IRequestExecutor tester = _cache.CreateSchema(_barNullableEntities, OnModelCreating); + + // act + // assert + IExecutionResult res1 = await tester.ExecuteAsync( + QueryRequestBuilder.New() + .SetQuery( + @" + { + root { + foo { + barString + objectArray { + foo { + barString + barShort + } + } + } + } + }") + .Create()); + + res1.MatchSqlSnapshot(); + } + + public static void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity().HasMany(x => x.ObjectArray); + modelBuilder.Entity().HasOne(x => x.NestedObject); + modelBuilder.Entity().HasOne(x => x.Foo); + } + + public class Foo + { + public int Id { get; set; } + + public short BarShort { get; set; } + + public string BarString { get; set; } = string.Empty; + + public BarEnum BarEnum { get; set; } + + public bool BarBool { get; set; } + + [UseSingleOrDefault] + public List ObjectArray { get; set; } + + public BarDeep NestedObject { get; set; } + } + + public class FooDeep + { + public int Id { get; set; } + + public short BarShort { get; set; } + + public string BarString { get; set; } = string.Empty; + } + + public class FooNullable + { + public int Id { get; set; } + + public short? BarShort { get; set; } + + public string? BarString { get; set; } + + public BarEnum? BarEnum { get; set; } + + public bool? BarBool { get; set; } + + [UseSingleOrDefault] + public List? ObjectArray { get; set; } + + public BarNullableDeep? NestedObject { get; set; } + } + + public class Bar + { + public int Id { get; set; } + + public Foo Foo { get; set; } + } + + public class BarDeep + { + public int Id { get; set; } + + public FooDeep Foo { get; set; } + } + + public class BarNullableDeep + { + public int Id { get; set; } + + public FooDeep? Foo { get; set; } + } + + public class BarNullable + { + public int Id { get; set; } + + public FooNullable? Foo { get; set; } + } + + public enum BarEnum + { + FOO, + BAR, + BAZ, + QUX + } + } +} diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/SchemaCache.cs b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/SchemaCache.cs new file mode 100644 index 00000000000..1659104a639 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/SchemaCache.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Concurrent; +using HotChocolate.Execution; +using HotChocolate.Types; +using Microsoft.EntityFrameworkCore; + +namespace HotChocolate.Data.Projections +{ + public class SchemaCache + : ProjectionVisitorTestBase, + IDisposable + { + private readonly ConcurrentDictionary<(Type, object), IRequestExecutor> _cache = + new ConcurrentDictionary<(Type, object), IRequestExecutor>(); + + public IRequestExecutor CreateSchema( + T[] entities, + Action? onModelCreating = null, + bool usePaging = false, + ObjectType? objectType = null) + where T : class + { + (Type, T[] entites) key = (typeof(T), entities); + return _cache.GetOrAdd( + key, + k => base.CreateSchema( + entities, + usePaging: usePaging, + onModelCreating: onModelCreating, + objectType: objectType)); + } + + public void Dispose() + { + } + } +} diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableFirstOrDefaultTests.Create_DeepFilterObjectTwoProjections.snap b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableFirstOrDefaultTests.Create_DeepFilterObjectTwoProjections.snap new file mode 100644 index 00000000000..54349513174 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableFirstOrDefaultTests.Create_DeepFilterObjectTwoProjections.snap @@ -0,0 +1,19 @@ +{ + "errors": [ + { + "message": "Unexpected Execution Error", + "locations": [ + { + "line": 3, + "column": 29 + } + ], + "path": [ + "root" + ] + } + ], + "data": { + "root": null + } +} diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableFirstOrDefaultTests.Create_DeepFilterObjectTwoProjections_Nullable.snap b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableFirstOrDefaultTests.Create_DeepFilterObjectTwoProjections_Nullable.snap new file mode 100644 index 00000000000..54349513174 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableFirstOrDefaultTests.Create_DeepFilterObjectTwoProjections_Nullable.snap @@ -0,0 +1,19 @@ +{ + "errors": [ + { + "message": "Unexpected Execution Error", + "locations": [ + { + "line": 3, + "column": 29 + } + ], + "path": [ + "root" + ] + } + ], + "data": { + "root": null + } +} diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableFirstOrDefaultTests.Create_DeepFilterObjectTwoProjections_Nullable__sql.snap b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableFirstOrDefaultTests.Create_DeepFilterObjectTwoProjections_Nullable__sql.snap new file mode 100644 index 00000000000..85c5af4a707 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableFirstOrDefaultTests.Create_DeepFilterObjectTwoProjections_Nullable__sql.snap @@ -0,0 +1 @@ +Translating this query requires APPLY operation which is not supported on SQLite. diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableFirstOrDefaultTests.Create_DeepFilterObjectTwoProjections__sql.snap b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableFirstOrDefaultTests.Create_DeepFilterObjectTwoProjections__sql.snap new file mode 100644 index 00000000000..85c5af4a707 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableFirstOrDefaultTests.Create_DeepFilterObjectTwoProjections__sql.snap @@ -0,0 +1 @@ +Translating this query requires APPLY operation which is not supported on SQLite. diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableFirstOrDefaultTests.Create_ListObjectDifferentLevelProjection.snap b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableFirstOrDefaultTests.Create_ListObjectDifferentLevelProjection.snap new file mode 100644 index 00000000000..54349513174 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableFirstOrDefaultTests.Create_ListObjectDifferentLevelProjection.snap @@ -0,0 +1,19 @@ +{ + "errors": [ + { + "message": "Unexpected Execution Error", + "locations": [ + { + "line": 3, + "column": 29 + } + ], + "path": [ + "root" + ] + } + ], + "data": { + "root": null + } +} diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableFirstOrDefaultTests.Create_ListObjectDifferentLevelProjection_Nullable.snap b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableFirstOrDefaultTests.Create_ListObjectDifferentLevelProjection_Nullable.snap new file mode 100644 index 00000000000..54349513174 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableFirstOrDefaultTests.Create_ListObjectDifferentLevelProjection_Nullable.snap @@ -0,0 +1,19 @@ +{ + "errors": [ + { + "message": "Unexpected Execution Error", + "locations": [ + { + "line": 3, + "column": 29 + } + ], + "path": [ + "root" + ] + } + ], + "data": { + "root": null + } +} diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableFirstOrDefaultTests.Create_ListObjectDifferentLevelProjection_Nullable__sql.snap b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableFirstOrDefaultTests.Create_ListObjectDifferentLevelProjection_Nullable__sql.snap new file mode 100644 index 00000000000..85c5af4a707 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableFirstOrDefaultTests.Create_ListObjectDifferentLevelProjection_Nullable__sql.snap @@ -0,0 +1 @@ +Translating this query requires APPLY operation which is not supported on SQLite. diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableFirstOrDefaultTests.Create_ListObjectDifferentLevelProjection__sql.snap b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableFirstOrDefaultTests.Create_ListObjectDifferentLevelProjection__sql.snap new file mode 100644 index 00000000000..85c5af4a707 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableFirstOrDefaultTests.Create_ListObjectDifferentLevelProjection__sql.snap @@ -0,0 +1 @@ +Translating this query requires APPLY operation which is not supported on SQLite. diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionFilterTests.Create_DeepFilterObjectTwoProjections.snap b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionFilterTests.Create_DeepFilterObjectTwoProjections.snap new file mode 100644 index 00000000000..0948cea3290 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionFilterTests.Create_DeepFilterObjectTwoProjections.snap @@ -0,0 +1,23 @@ +{ + "data": { + "root": [ + { + "foo": { + "objectArray": [ + { + "foo": { + "barString": "a", + "barShort": 12 + } + } + ] + } + }, + { + "foo": { + "objectArray": [] + } + } + ] + } +} diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionFilterTests.Create_DeepFilterObjectTwoProjections_Nullable.snap b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionFilterTests.Create_DeepFilterObjectTwoProjections_Nullable.snap new file mode 100644 index 00000000000..308351f4bd9 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionFilterTests.Create_DeepFilterObjectTwoProjections_Nullable.snap @@ -0,0 +1,26 @@ +{ + "data": { + "root": [ + { + "foo": { + "objectArray": [] + } + }, + { + "foo": { + "objectArray": [] + } + }, + { + "foo": { + "objectArray": [] + } + }, + { + "foo": { + "objectArray": [] + } + } + ] + } +} diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionFilterTests.Create_DeepFilterObjectTwoProjections_Nullable__sql.snap b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionFilterTests.Create_DeepFilterObjectTwoProjections_Nullable__sql.snap new file mode 100644 index 00000000000..65bbd78b3d1 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionFilterTests.Create_DeepFilterObjectTwoProjections_Nullable__sql.snap @@ -0,0 +1,12 @@ +.param set @__p_0 'a' + +SELECT "d"."Id", "f"."Id", "t"."BarString", "t"."BarShort", "t"."Id", "t"."Id0" +FROM "Data" AS "d" +LEFT JOIN "FooNullable" AS "f" ON "d"."FooId" = "f"."Id" +LEFT JOIN ( + SELECT "f0"."BarString", "f0"."BarShort", "b"."Id", "f0"."Id" AS "Id0", "b"."FooNullableId" + FROM "BarNullableDeep" AS "b" + LEFT JOIN "FooDeep" AS "f0" ON "b"."FooId" = "f0"."Id" + WHERE "f0"."BarString" = @__p_0 +) AS "t" ON "f"."Id" = "t"."FooNullableId" +ORDER BY "d"."Id", "f"."Id", "t"."Id", "t"."Id0" diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionFilterTests.Create_DeepFilterObjectTwoProjections__sql.snap b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionFilterTests.Create_DeepFilterObjectTwoProjections__sql.snap new file mode 100644 index 00000000000..7bbb1cf4320 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionFilterTests.Create_DeepFilterObjectTwoProjections__sql.snap @@ -0,0 +1,12 @@ +.param set @__p_0 'a' + +SELECT "d"."Id", "f"."Id", "t"."BarString", "t"."BarShort", "t"."Id", "t"."Id0" +FROM "Data" AS "d" +LEFT JOIN "Foo" AS "f" ON "d"."FooId" = "f"."Id" +LEFT JOIN ( + SELECT "f0"."BarString", "f0"."BarShort", "b"."Id", "f0"."Id" AS "Id0", "b"."FooId" + FROM "BarDeep" AS "b" + LEFT JOIN "FooDeep" AS "f0" ON "b"."FooId1" = "f0"."Id" + WHERE "f0"."BarString" = @__p_0 +) AS "t" ON "f"."Id" = "t"."FooId" +ORDER BY "d"."Id", "f"."Id", "t"."Id", "t"."Id0" diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionFilterTests.Create_ListObjectDifferentLevelProjection.snap b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionFilterTests.Create_ListObjectDifferentLevelProjection.snap new file mode 100644 index 00000000000..348e0cd67bc --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionFilterTests.Create_ListObjectDifferentLevelProjection.snap @@ -0,0 +1,25 @@ +{ + "data": { + "root": [ + { + "foo": { + "barString": "testatest", + "objectArray": [ + { + "foo": { + "barString": "a", + "barShort": 12 + } + } + ] + } + }, + { + "foo": { + "barString": "testbtest", + "objectArray": [] + } + } + ] + } +} diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionFilterTests.Create_ListObjectDifferentLevelProjection_Nullable.snap b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionFilterTests.Create_ListObjectDifferentLevelProjection_Nullable.snap new file mode 100644 index 00000000000..5d6b58425d9 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionFilterTests.Create_ListObjectDifferentLevelProjection_Nullable.snap @@ -0,0 +1,30 @@ +{ + "data": { + "root": [ + { + "foo": { + "barString": "testatest", + "objectArray": [] + } + }, + { + "foo": { + "barString": "testbtest", + "objectArray": [] + } + }, + { + "foo": { + "barString": "testctest", + "objectArray": [] + } + }, + { + "foo": { + "barString": "testdtest", + "objectArray": [] + } + } + ] + } +} diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionFilterTests.Create_ListObjectDifferentLevelProjection_Nullable__sql.snap b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionFilterTests.Create_ListObjectDifferentLevelProjection_Nullable__sql.snap new file mode 100644 index 00000000000..70f3b9f273d --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionFilterTests.Create_ListObjectDifferentLevelProjection_Nullable__sql.snap @@ -0,0 +1,12 @@ +.param set @__p_0 'a' + +SELECT "f"."BarString", "d"."Id", "f"."Id", "t"."BarString", "t"."BarShort", "t"."Id", "t"."Id0" +FROM "Data" AS "d" +LEFT JOIN "FooNullable" AS "f" ON "d"."FooId" = "f"."Id" +LEFT JOIN ( + SELECT "f0"."BarString", "f0"."BarShort", "b"."Id", "f0"."Id" AS "Id0", "b"."FooNullableId" + FROM "BarNullableDeep" AS "b" + LEFT JOIN "FooDeep" AS "f0" ON "b"."FooId" = "f0"."Id" + WHERE "f0"."BarString" = @__p_0 +) AS "t" ON "f"."Id" = "t"."FooNullableId" +ORDER BY "d"."Id", "f"."Id", "t"."Id", "t"."Id0" diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionFilterTests.Create_ListObjectDifferentLevelProjection__sql.snap b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionFilterTests.Create_ListObjectDifferentLevelProjection__sql.snap new file mode 100644 index 00000000000..94dd2ff6e51 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionFilterTests.Create_ListObjectDifferentLevelProjection__sql.snap @@ -0,0 +1,12 @@ +.param set @__p_0 'a' + +SELECT "f"."BarString", "d"."Id", "f"."Id", "t"."BarString", "t"."BarShort", "t"."Id", "t"."Id0" +FROM "Data" AS "d" +LEFT JOIN "Foo" AS "f" ON "d"."FooId" = "f"."Id" +LEFT JOIN ( + SELECT "f0"."BarString", "f0"."BarShort", "b"."Id", "f0"."Id" AS "Id0", "b"."FooId" + FROM "BarDeep" AS "b" + LEFT JOIN "FooDeep" AS "f0" ON "b"."FooId1" = "f0"."Id" + WHERE "f0"."BarString" = @__p_0 +) AS "t" ON "f"."Id" = "t"."FooId" +ORDER BY "d"."Id", "f"."Id", "t"."Id", "t"."Id0" diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionHashSetTests.Create_DeepFilterObjectTwoProjections.snap b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionHashSetTests.Create_DeepFilterObjectTwoProjections.snap new file mode 100644 index 00000000000..54349513174 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionHashSetTests.Create_DeepFilterObjectTwoProjections.snap @@ -0,0 +1,19 @@ +{ + "errors": [ + { + "message": "Unexpected Execution Error", + "locations": [ + { + "line": 3, + "column": 29 + } + ], + "path": [ + "root" + ] + } + ], + "data": { + "root": null + } +} diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionHashSetTests.Create_ListObjectDifferentLevelProjection.snap b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionHashSetTests.Create_ListObjectDifferentLevelProjection.snap new file mode 100644 index 00000000000..54349513174 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionHashSetTests.Create_ListObjectDifferentLevelProjection.snap @@ -0,0 +1,19 @@ +{ + "errors": [ + { + "message": "Unexpected Execution Error", + "locations": [ + { + "line": 3, + "column": 29 + } + ], + "path": [ + "root" + ] + } + ], + "data": { + "root": null + } +} diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionISetTests.Create_DeepFilterObjectTwoProjections.snap b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionISetTests.Create_DeepFilterObjectTwoProjections.snap new file mode 100644 index 00000000000..54349513174 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionISetTests.Create_DeepFilterObjectTwoProjections.snap @@ -0,0 +1,19 @@ +{ + "errors": [ + { + "message": "Unexpected Execution Error", + "locations": [ + { + "line": 3, + "column": 29 + } + ], + "path": [ + "root" + ] + } + ], + "data": { + "root": null + } +} diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionISetTests.Create_ListObjectDifferentLevelProjection.snap b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionISetTests.Create_ListObjectDifferentLevelProjection.snap new file mode 100644 index 00000000000..54349513174 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionISetTests.Create_ListObjectDifferentLevelProjection.snap @@ -0,0 +1,19 @@ +{ + "errors": [ + { + "message": "Unexpected Execution Error", + "locations": [ + { + "line": 3, + "column": 29 + } + ], + "path": [ + "root" + ] + } + ], + "data": { + "root": null + } +} diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionSortedSetTests.Create_DeepFilterObjectTwoProjections.snap b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionSortedSetTests.Create_DeepFilterObjectTwoProjections.snap new file mode 100644 index 00000000000..54349513174 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionSortedSetTests.Create_DeepFilterObjectTwoProjections.snap @@ -0,0 +1,19 @@ +{ + "errors": [ + { + "message": "Unexpected Execution Error", + "locations": [ + { + "line": 3, + "column": 29 + } + ], + "path": [ + "root" + ] + } + ], + "data": { + "root": null + } +} diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionSortedSetTests.Create_ListObjectDifferentLevelProjection.snap b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionSortedSetTests.Create_ListObjectDifferentLevelProjection.snap new file mode 100644 index 00000000000..54349513174 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionSortedSetTests.Create_ListObjectDifferentLevelProjection.snap @@ -0,0 +1,19 @@ +{ + "errors": [ + { + "message": "Unexpected Execution Error", + "locations": [ + { + "line": 3, + "column": 29 + } + ], + "path": [ + "root" + ] + } + ], + "data": { + "root": null + } +} diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionSortingTests.Create_DeepFilterObjectTwoProjections.snap b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionSortingTests.Create_DeepFilterObjectTwoProjections.snap new file mode 100644 index 00000000000..67c4ac713b8 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionSortingTests.Create_DeepFilterObjectTwoProjections.snap @@ -0,0 +1,54 @@ +{ + "data": { + "root": [ + { + "foo": { + "objectArray": [ + { + "foo": { + "barString": "a", + "barShort": 1 + } + }, + { + "foo": { + "barString": "a", + "barShort": 3 + } + }, + { + "foo": { + "barString": "a", + "barShort": 12 + } + } + ] + } + }, + { + "foo": { + "objectArray": [ + { + "foo": { + "barString": "a", + "barShort": 1 + } + }, + { + "foo": { + "barString": "a", + "barShort": 3 + } + }, + { + "foo": { + "barString": "a", + "barShort": 12 + } + } + ] + } + } + ] + } +} diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionSortingTests.Create_DeepFilterObjectTwoProjections_Nullable.snap b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionSortingTests.Create_DeepFilterObjectTwoProjections_Nullable.snap new file mode 100644 index 00000000000..07b2fde4142 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionSortingTests.Create_DeepFilterObjectTwoProjections_Nullable.snap @@ -0,0 +1,23 @@ +{ + "errors": [ + { + "message": "The argument \u0060order\u0060 does not exist.", + "locations": [ + { + "line": 6, + "column": 41 + } + ], + "path": [ + "root", + "foo" + ], + "extensions": { + "type": "FooNullable", + "field": "objectArray", + "argument": "order", + "specifiedBy": "http://spec.graphql.org/June2018/#sec-Required-Arguments" + } + } + ] +} diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionSortingTests.Create_DeepFilterObjectTwoProjections__sql.snap b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionSortingTests.Create_DeepFilterObjectTwoProjections__sql.snap new file mode 100644 index 00000000000..afa6a72170e --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionSortingTests.Create_DeepFilterObjectTwoProjections__sql.snap @@ -0,0 +1,9 @@ +SELECT "d"."Id", "f"."Id", "t"."BarString", "t"."BarShort", "t"."Id", "t"."Id0" +FROM "Data" AS "d" +LEFT JOIN "Foo" AS "f" ON "d"."FooId" = "f"."Id" +LEFT JOIN ( + SELECT "f0"."BarString", "f0"."BarShort", "b"."Id", "f0"."Id" AS "Id0", "b"."FooId" + FROM "BarDeep" AS "b" + LEFT JOIN "FooDeep" AS "f0" ON "b"."FooId1" = "f0"."Id" +) AS "t" ON "f"."Id" = "t"."FooId" +ORDER BY "d"."Id", "f"."Id", "t"."BarShort", "t"."Id", "t"."Id0" diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionSortingTests.Create_ListObjectDifferentLevelProjection.snap b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionSortingTests.Create_ListObjectDifferentLevelProjection.snap new file mode 100644 index 00000000000..57529343192 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionSortingTests.Create_ListObjectDifferentLevelProjection.snap @@ -0,0 +1,56 @@ +{ + "data": { + "root": [ + { + "foo": { + "barString": "testatest", + "objectArray": [ + { + "foo": { + "barString": "a", + "barShort": 1 + } + }, + { + "foo": { + "barString": "a", + "barShort": 3 + } + }, + { + "foo": { + "barString": "a", + "barShort": 12 + } + } + ] + } + }, + { + "foo": { + "barString": "testbtest", + "objectArray": [ + { + "foo": { + "barString": "a", + "barShort": 1 + } + }, + { + "foo": { + "barString": "a", + "barShort": 3 + } + }, + { + "foo": { + "barString": "a", + "barShort": 12 + } + } + ] + } + } + ] + } +} diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionSortingTests.Create_ListObjectDifferentLevelProjection_Nullable.snap b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionSortingTests.Create_ListObjectDifferentLevelProjection_Nullable.snap new file mode 100644 index 00000000000..414241f7e88 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionSortingTests.Create_ListObjectDifferentLevelProjection_Nullable.snap @@ -0,0 +1,23 @@ +{ + "errors": [ + { + "message": "The argument \u0060order\u0060 does not exist.", + "locations": [ + { + "line": 7, + "column": 41 + } + ], + "path": [ + "root", + "foo" + ], + "extensions": { + "type": "FooNullable", + "field": "objectArray", + "argument": "order", + "specifiedBy": "http://spec.graphql.org/June2018/#sec-Required-Arguments" + } + } + ] +} diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionSortingTests.Create_ListObjectDifferentLevelProjection__sql.snap b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionSortingTests.Create_ListObjectDifferentLevelProjection__sql.snap new file mode 100644 index 00000000000..903d09f6b8d --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionSortingTests.Create_ListObjectDifferentLevelProjection__sql.snap @@ -0,0 +1,9 @@ +SELECT "f"."BarString", "d"."Id", "f"."Id", "t"."BarString", "t"."BarShort", "t"."Id", "t"."Id0" +FROM "Data" AS "d" +LEFT JOIN "Foo" AS "f" ON "d"."FooId" = "f"."Id" +LEFT JOIN ( + SELECT "f0"."BarString", "f0"."BarShort", "b"."Id", "f0"."Id" AS "Id0", "b"."FooId" + FROM "BarDeep" AS "b" + LEFT JOIN "FooDeep" AS "f0" ON "b"."FooId1" = "f0"."Id" +) AS "t" ON "f"."Id" = "t"."FooId" +ORDER BY "d"."Id", "f"."Id", "t"."BarShort", "t"."Id", "t"."Id0" diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorIsProjectedTests.IsProjected_Should_AlwaysBeProjectedWhenSelected_When_True.snap b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorIsProjectedTests.IsProjected_Should_AlwaysBeProjectedWhenSelected_When_True.snap new file mode 100644 index 00000000000..bae5ddebc76 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorIsProjectedTests.IsProjected_Should_AlwaysBeProjectedWhenSelected_When_True.snap @@ -0,0 +1,12 @@ +{ + "data": { + "root": [ + { + "isProjectedFalse": null + }, + { + "isProjectedFalse": null + } + ] + } +} diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorIsProjectedTests.IsProjected_Should_AlwaysBeProjectedWhenSelected_When_True__sql.snap b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorIsProjectedTests.IsProjected_Should_AlwaysBeProjectedWhenSelected_When_True__sql.snap new file mode 100644 index 00000000000..89804118c71 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorIsProjectedTests.IsProjected_Should_AlwaysBeProjectedWhenSelected_When_True__sql.snap @@ -0,0 +1,2 @@ +SELECT "d"."IsProjectedTrue" +FROM "Data" AS "d" diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorIsProjectedTests.IsProjected_Should_NotBeProjectedWhenSelected_When_FalseWithOneProps.snap b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorIsProjectedTests.IsProjected_Should_NotBeProjectedWhenSelected_When_FalseWithOneProps.snap new file mode 100644 index 00000000000..bae5ddebc76 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorIsProjectedTests.IsProjected_Should_NotBeProjectedWhenSelected_When_FalseWithOneProps.snap @@ -0,0 +1,12 @@ +{ + "data": { + "root": [ + { + "isProjectedFalse": null + }, + { + "isProjectedFalse": null + } + ] + } +} diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorIsProjectedTests.IsProjected_Should_NotBeProjectedWhenSelected_When_FalseWithOneProps__sql.snap b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorIsProjectedTests.IsProjected_Should_NotBeProjectedWhenSelected_When_FalseWithOneProps__sql.snap new file mode 100644 index 00000000000..89804118c71 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorIsProjectedTests.IsProjected_Should_NotBeProjectedWhenSelected_When_FalseWithOneProps__sql.snap @@ -0,0 +1,2 @@ +SELECT "d"."IsProjectedTrue" +FROM "Data" AS "d" diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorIsProjectedTests.IsProjected_Should_NotBeProjectedWhenSelected_When_FalseWithTwoProps.snap b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorIsProjectedTests.IsProjected_Should_NotBeProjectedWhenSelected_When_FalseWithTwoProps.snap new file mode 100644 index 00000000000..93384dfeb80 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorIsProjectedTests.IsProjected_Should_NotBeProjectedWhenSelected_When_FalseWithTwoProps.snap @@ -0,0 +1,14 @@ +{ + "data": { + "root": [ + { + "isProjectedFalse": null, + "isProjectedTrue": true + }, + { + "isProjectedFalse": null, + "isProjectedTrue": true + } + ] + } +} diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorIsProjectedTests.IsProjected_Should_NotBeProjectedWhenSelected_When_FalseWithTwoProps__sql.snap b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorIsProjectedTests.IsProjected_Should_NotBeProjectedWhenSelected_When_FalseWithTwoProps__sql.snap new file mode 100644 index 00000000000..89804118c71 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorIsProjectedTests.IsProjected_Should_NotBeProjectedWhenSelected_When_FalseWithTwoProps__sql.snap @@ -0,0 +1,2 @@ +SELECT "d"."IsProjectedTrue" +FROM "Data" AS "d" diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorIsProjectedTests.IsProjected_Should_NotFailWhenSelectionSetSkippedCompletely.snap b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorIsProjectedTests.IsProjected_Should_NotFailWhenSelectionSetSkippedCompletely.snap new file mode 100644 index 00000000000..bae5ddebc76 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorIsProjectedTests.IsProjected_Should_NotFailWhenSelectionSetSkippedCompletely.snap @@ -0,0 +1,12 @@ +{ + "data": { + "root": [ + { + "isProjectedFalse": null + }, + { + "isProjectedFalse": null + } + ] + } +} diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorIsProjectedTests.IsProjected_Should_NotFailWhenSelectionSetSkippedCompletely__sql.snap b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorIsProjectedTests.IsProjected_Should_NotFailWhenSelectionSetSkippedCompletely__sql.snap new file mode 100644 index 00000000000..65477d6e455 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorIsProjectedTests.IsProjected_Should_NotFailWhenSelectionSetSkippedCompletely__sql.snap @@ -0,0 +1,2 @@ +SELECT 1 +FROM "Data" AS "d" diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorObjectTests.Create_ListObjectDifferentLevelProjection_12.snap b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorObjectTests.Create_ListObjectDifferentLevelProjection_12.snap new file mode 100644 index 00000000000..b57a86b8336 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorObjectTests.Create_ListObjectDifferentLevelProjection_12.snap @@ -0,0 +1,30 @@ +{ + "data": { + "root": [ + { + "foo": { + "barString": "testatest", + "objectArray": [ + { + "foo": { + "barShort": 12 + } + } + ] + } + }, + { + "foo": { + "barString": "testbtest", + "objectArray": [ + { + "foo": { + "barShort": 14 + } + } + ] + } + } + ] + } +} diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorObjectTests.Create_ListObjectDifferentLevelProjection_12_sql.snap b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorObjectTests.Create_ListObjectDifferentLevelProjection_12_sql.snap new file mode 100644 index 00000000000..b83d72a59f7 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorObjectTests.Create_ListObjectDifferentLevelProjection_12_sql.snap @@ -0,0 +1,9 @@ +SELECT "f"."BarString", "d"."Id", "t"."BarShort", "t"."Id" +FROM "Data" AS "d" +LEFT JOIN "Foo" AS "f" ON "d"."FooId" = "f"."Id" +LEFT JOIN ( + SELECT "f0"."BarShort", "b"."Id", "b"."FooId" + FROM "BarDeep" AS "b" + LEFT JOIN "FooDeep" AS "f0" ON "b"."FooId1" = "f0"."Id" +) AS "t" ON "f"."Id" = "t"."FooId" +ORDER BY "d"."Id", "t"."Id" diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorObjectTests.Create_ListObjectTwoProjections_12.snap b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorObjectTests.Create_ListObjectTwoProjections_12.snap new file mode 100644 index 00000000000..3452a3126d1 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorObjectTests.Create_ListObjectTwoProjections_12.snap @@ -0,0 +1,30 @@ +{ + "data": { + "root": [ + { + "foo": { + "objectArray": [ + { + "foo": { + "barString": "a", + "barShort": 12 + } + } + ] + } + }, + { + "foo": { + "objectArray": [ + { + "foo": { + "barString": "d", + "barShort": 14 + } + } + ] + } + } + ] + } +} diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorObjectTests.Create_ListObjectTwoProjections_12_sql.snap b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorObjectTests.Create_ListObjectTwoProjections_12_sql.snap new file mode 100644 index 00000000000..d05266c20ca --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorObjectTests.Create_ListObjectTwoProjections_12_sql.snap @@ -0,0 +1,9 @@ +SELECT "d"."Id", "t"."BarString", "t"."BarShort", "t"."Id" +FROM "Data" AS "d" +LEFT JOIN "Foo" AS "f" ON "d"."FooId" = "f"."Id" +LEFT JOIN ( + SELECT "f0"."BarString", "f0"."BarShort", "b"."Id", "b"."FooId" + FROM "BarDeep" AS "b" + LEFT JOIN "FooDeep" AS "f0" ON "b"."FooId1" = "f0"."Id" +) AS "t" ON "f"."Id" = "t"."FooId" +ORDER BY "d"."Id", "t"."Id" diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorObjectTests.Create_NestedObjectDifferentLevelProjection_12.snap b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorObjectTests.Create_NestedObjectDifferentLevelProjection_12.snap new file mode 100644 index 00000000000..1de85d62620 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorObjectTests.Create_NestedObjectDifferentLevelProjection_12.snap @@ -0,0 +1,26 @@ +{ + "data": { + "root": [ + { + "foo": { + "barString": "testatest", + "nestedObject": { + "foo": { + "barShort": 12 + } + } + } + }, + { + "foo": { + "barString": "testbtest", + "nestedObject": { + "foo": { + "barShort": 12 + } + } + } + } + ] + } +} diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorObjectTests.Create_NestedObjectDifferentLevelProjection_12_sql.snap b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorObjectTests.Create_NestedObjectDifferentLevelProjection_12_sql.snap new file mode 100644 index 00000000000..b657b841a83 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorObjectTests.Create_NestedObjectDifferentLevelProjection_12_sql.snap @@ -0,0 +1,5 @@ +SELECT "f"."BarString", "f0"."BarShort" +FROM "Data" AS "d" +LEFT JOIN "Foo" AS "f" ON "d"."FooId" = "f"."Id" +LEFT JOIN "BarDeep" AS "b" ON "f"."NestedObjectId" = "b"."Id" +LEFT JOIN "FooDeep" AS "f0" ON "b"."FooId1" = "f0"."Id" diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorObjectTests.Create_NestedObjectTwoProjections_12.snap b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorObjectTests.Create_NestedObjectTwoProjections_12.snap new file mode 100644 index 00000000000..6b1186f64b8 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorObjectTests.Create_NestedObjectTwoProjections_12.snap @@ -0,0 +1,26 @@ +{ + "data": { + "root": [ + { + "foo": { + "nestedObject": { + "foo": { + "barString": "a", + "barShort": 12 + } + } + } + }, + { + "foo": { + "nestedObject": { + "foo": { + "barString": "d", + "barShort": 12 + } + } + } + } + ] + } +} diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorObjectTests.Create_NestedObjectTwoProjections_12_sql.snap b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorObjectTests.Create_NestedObjectTwoProjections_12_sql.snap new file mode 100644 index 00000000000..6964b087782 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorObjectTests.Create_NestedObjectTwoProjections_12_sql.snap @@ -0,0 +1,5 @@ +SELECT "f0"."BarString", "f0"."BarShort" +FROM "Data" AS "d" +LEFT JOIN "Foo" AS "f" ON "d"."FooId" = "f"."Id" +LEFT JOIN "BarDeep" AS "b" ON "f"."NestedObjectId" = "b"."Id" +LEFT JOIN "FooDeep" AS "f0" ON "b"."FooId1" = "f0"."Id" diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorObjectTests.Create_ObjectSingleProjection_12.snap b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorObjectTests.Create_ObjectSingleProjection_12.snap new file mode 100644 index 00000000000..43424163d38 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorObjectTests.Create_ObjectSingleProjection_12.snap @@ -0,0 +1,16 @@ +{ + "data": { + "root": [ + { + "foo": { + "barShort": 12 + } + }, + { + "foo": { + "barShort": 14 + } + } + ] + } +} diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorObjectTests.Create_ObjectSingleProjection_12_sql.snap b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorObjectTests.Create_ObjectSingleProjection_12_sql.snap new file mode 100644 index 00000000000..b3f3591c468 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorObjectTests.Create_ObjectSingleProjection_12_sql.snap @@ -0,0 +1,3 @@ +SELECT "f"."BarShort" +FROM "Data" AS "d" +LEFT JOIN "Foo" AS "f" ON "d"."FooId" = "f"."Id" diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorObjectTests.Create_ObjectTwoProjections_12.snap b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorObjectTests.Create_ObjectTwoProjections_12.snap new file mode 100644 index 00000000000..f94ec328806 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorObjectTests.Create_ObjectTwoProjections_12.snap @@ -0,0 +1,18 @@ +{ + "data": { + "root": [ + { + "foo": { + "barString": "testatest", + "barShort": 12 + } + }, + { + "foo": { + "barString": "testbtest", + "barShort": 14 + } + } + ] + } +} diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorObjectTests.Create_ObjectTwoProjections_12_sql.snap b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorObjectTests.Create_ObjectTwoProjections_12_sql.snap new file mode 100644 index 00000000000..06e0b2338d2 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorObjectTests.Create_ObjectTwoProjections_12_sql.snap @@ -0,0 +1,3 @@ +SELECT "f"."BarString", "f"."BarShort" +FROM "Data" AS "d" +LEFT JOIN "Foo" AS "f" ON "d"."FooId" = "f"."Id" diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorPagingTests.CreateNullable_ProjectsOneProperty_Edges.snap b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorPagingTests.CreateNullable_ProjectsOneProperty_Edges.snap new file mode 100644 index 00000000000..305ea9bf2d8 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorPagingTests.CreateNullable_ProjectsOneProperty_Edges.snap @@ -0,0 +1,23 @@ +{ + "data": { + "root": { + "edges": [ + { + "node": { + "baz": "a" + } + }, + { + "node": { + "baz": null + } + }, + { + "node": { + "baz": "c" + } + } + ] + } + } +} diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorPagingTests.CreateNullable_ProjectsOneProperty_EdgesAndNodesOverlap.snap b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorPagingTests.CreateNullable_ProjectsOneProperty_EdgesAndNodesOverlap.snap new file mode 100644 index 00000000000..1b52a4612a2 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorPagingTests.CreateNullable_ProjectsOneProperty_EdgesAndNodesOverlap.snap @@ -0,0 +1,34 @@ +{ + "data": { + "root": { + "nodes": [ + { + "baz": "a" + }, + { + "baz": null + }, + { + "baz": "c" + } + ], + "edges": [ + { + "node": { + "baz": "a" + } + }, + { + "node": { + "baz": null + } + }, + { + "node": { + "baz": "c" + } + } + ] + } + } +} diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorPagingTests.CreateNullable_ProjectsOneProperty_EdgesAndNodesOverlap__sql.snap b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorPagingTests.CreateNullable_ProjectsOneProperty_EdgesAndNodesOverlap__sql.snap new file mode 100644 index 00000000000..b60ee95c3d8 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorPagingTests.CreateNullable_ProjectsOneProperty_EdgesAndNodesOverlap__sql.snap @@ -0,0 +1,2 @@ +SELECT "d"."Baz" +FROM "Data" AS "d" diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorPagingTests.CreateNullable_ProjectsOneProperty_Edges__sql.snap b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorPagingTests.CreateNullable_ProjectsOneProperty_Edges__sql.snap new file mode 100644 index 00000000000..b60ee95c3d8 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorPagingTests.CreateNullable_ProjectsOneProperty_Edges__sql.snap @@ -0,0 +1,2 @@ +SELECT "d"."Baz" +FROM "Data" AS "d" diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorPagingTests.CreateNullable_ProjectsOneProperty_Nodes.snap b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorPagingTests.CreateNullable_ProjectsOneProperty_Nodes.snap new file mode 100644 index 00000000000..c558f6e442c --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorPagingTests.CreateNullable_ProjectsOneProperty_Nodes.snap @@ -0,0 +1,17 @@ +{ + "data": { + "root": { + "nodes": [ + { + "baz": "a" + }, + { + "baz": null + }, + { + "baz": "c" + } + ] + } + } +} diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorPagingTests.CreateNullable_ProjectsOneProperty_Nodes__sql.snap b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorPagingTests.CreateNullable_ProjectsOneProperty_Nodes__sql.snap new file mode 100644 index 00000000000..b60ee95c3d8 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorPagingTests.CreateNullable_ProjectsOneProperty_Nodes__sql.snap @@ -0,0 +1,2 @@ +SELECT "d"."Baz" +FROM "Data" AS "d" diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorPagingTests.CreateNullable_ProjectsTwoProperties_Edges.snap b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorPagingTests.CreateNullable_ProjectsTwoProperties_Edges.snap new file mode 100644 index 00000000000..207b6b65015 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorPagingTests.CreateNullable_ProjectsTwoProperties_Edges.snap @@ -0,0 +1,26 @@ +{ + "data": { + "root": { + "edges": [ + { + "node": { + "bar": true, + "baz": "a" + } + }, + { + "node": { + "bar": null, + "baz": null + } + }, + { + "node": { + "bar": false, + "baz": "c" + } + } + ] + } + } +} diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorPagingTests.CreateNullable_ProjectsTwoProperties_EdgesAndNodes.snap b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorPagingTests.CreateNullable_ProjectsTwoProperties_EdgesAndNodes.snap new file mode 100644 index 00000000000..3600ad1466e --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorPagingTests.CreateNullable_ProjectsTwoProperties_EdgesAndNodes.snap @@ -0,0 +1,37 @@ +{ + "data": { + "root": { + "nodes": [ + { + "bar": true, + "baz": "a" + }, + { + "bar": null, + "baz": null + }, + { + "bar": false, + "baz": "c" + } + ], + "edges": [ + { + "node": { + "bar": true + } + }, + { + "node": { + "bar": null + } + }, + { + "node": { + "bar": false + } + } + ] + } + } +} diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorPagingTests.CreateNullable_ProjectsTwoProperties_EdgesAndNodes__sql.snap b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorPagingTests.CreateNullable_ProjectsTwoProperties_EdgesAndNodes__sql.snap new file mode 100644 index 00000000000..264538e12db --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorPagingTests.CreateNullable_ProjectsTwoProperties_EdgesAndNodes__sql.snap @@ -0,0 +1,2 @@ +SELECT "d"."Bar", "d"."Baz" +FROM "Data" AS "d" diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorPagingTests.CreateNullable_ProjectsTwoProperties_Edges__sql.snap b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorPagingTests.CreateNullable_ProjectsTwoProperties_Edges__sql.snap new file mode 100644 index 00000000000..264538e12db --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorPagingTests.CreateNullable_ProjectsTwoProperties_Edges__sql.snap @@ -0,0 +1,2 @@ +SELECT "d"."Bar", "d"."Baz" +FROM "Data" AS "d" diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorPagingTests.CreateNullable_ProjectsTwoProperties_Nodes.snap b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorPagingTests.CreateNullable_ProjectsTwoProperties_Nodes.snap new file mode 100644 index 00000000000..a9abf692f17 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorPagingTests.CreateNullable_ProjectsTwoProperties_Nodes.snap @@ -0,0 +1,20 @@ +{ + "data": { + "root": { + "nodes": [ + { + "bar": true, + "baz": "a" + }, + { + "bar": null, + "baz": null + }, + { + "bar": false, + "baz": "c" + } + ] + } + } +} diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorPagingTests.CreateNullable_ProjectsTwoProperties_Nodes__sql.snap b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorPagingTests.CreateNullable_ProjectsTwoProperties_Nodes__sql.snap new file mode 100644 index 00000000000..264538e12db --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorPagingTests.CreateNullable_ProjectsTwoProperties_Nodes__sql.snap @@ -0,0 +1,2 @@ +SELECT "d"."Bar", "d"."Baz" +FROM "Data" AS "d" diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorPagingTests.Create_Projection_Should_Stop_When_UsePagingEncountered.snap b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorPagingTests.Create_Projection_Should_Stop_When_UsePagingEncountered.snap new file mode 100644 index 00000000000..5c9960bad8e --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorPagingTests.Create_Projection_Should_Stop_When_UsePagingEncountered.snap @@ -0,0 +1,34 @@ +{ + "data": { + "root": { + "nodes": [ + { + "bar": true, + "paging": { + "nodes": [ + { + "barBaz": "a_a" + }, + { + "barBaz": "a_b" + } + ] + } + }, + { + "bar": false, + "paging": { + "nodes": [ + { + "barBaz": "a_a" + }, + { + "barBaz": "a_b" + } + ] + } + } + ] + } + } +} diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorPagingTests.Create_Projection_Should_Stop_When_UsePagingEncountered__sql.snap b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorPagingTests.Create_Projection_Should_Stop_When_UsePagingEncountered__sql.snap new file mode 100644 index 00000000000..c56a9350b96 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorPagingTests.Create_Projection_Should_Stop_When_UsePagingEncountered__sql.snap @@ -0,0 +1,2 @@ +SELECT "d"."Bar" +FROM "Data" AS "d" diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorPagingTests.Create_Projection_Should_Stop_When_UseProjectionEncountered.snap b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorPagingTests.Create_Projection_Should_Stop_When_UseProjectionEncountered.snap new file mode 100644 index 00000000000..92566b94cba --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorPagingTests.Create_Projection_Should_Stop_When_UseProjectionEncountered.snap @@ -0,0 +1,30 @@ +{ + "data": { + "root": { + "nodes": [ + { + "bar": true, + "list": [ + { + "barBaz": "a_a" + }, + { + "barBaz": "a_b" + } + ] + }, + { + "bar": false, + "list": [ + { + "barBaz": "a_a" + }, + { + "barBaz": "a_b" + } + ] + } + ] + } + } +} diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorPagingTests.Create_Projection_Should_Stop_When_UseProjectionEncountered__sql.snap b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorPagingTests.Create_Projection_Should_Stop_When_UseProjectionEncountered__sql.snap new file mode 100644 index 00000000000..c56a9350b96 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorPagingTests.Create_Projection_Should_Stop_When_UseProjectionEncountered__sql.snap @@ -0,0 +1,2 @@ +SELECT "d"."Bar" +FROM "Data" AS "d" diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorPagingTests.Create_ProjectsOneProperty_Edges.snap b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorPagingTests.Create_ProjectsOneProperty_Edges.snap new file mode 100644 index 00000000000..0e35cde3fe7 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorPagingTests.Create_ProjectsOneProperty_Edges.snap @@ -0,0 +1,18 @@ +{ + "data": { + "root": { + "edges": [ + { + "node": { + "baz": "a" + } + }, + { + "node": { + "baz": "b" + } + } + ] + } + } +} diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorPagingTests.Create_ProjectsOneProperty_EdgesAndNodesOverlap.snap b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorPagingTests.Create_ProjectsOneProperty_EdgesAndNodesOverlap.snap new file mode 100644 index 00000000000..77fb956c155 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorPagingTests.Create_ProjectsOneProperty_EdgesAndNodesOverlap.snap @@ -0,0 +1,26 @@ +{ + "data": { + "root": { + "nodes": [ + { + "baz": "a" + }, + { + "baz": "b" + } + ], + "edges": [ + { + "node": { + "baz": "a" + } + }, + { + "node": { + "baz": "b" + } + } + ] + } + } +} diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorPagingTests.Create_ProjectsOneProperty_EdgesAndNodesOverlap__sql.snap b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorPagingTests.Create_ProjectsOneProperty_EdgesAndNodesOverlap__sql.snap new file mode 100644 index 00000000000..b60ee95c3d8 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorPagingTests.Create_ProjectsOneProperty_EdgesAndNodesOverlap__sql.snap @@ -0,0 +1,2 @@ +SELECT "d"."Baz" +FROM "Data" AS "d" diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorPagingTests.Create_ProjectsOneProperty_Edges__sql.snap b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorPagingTests.Create_ProjectsOneProperty_Edges__sql.snap new file mode 100644 index 00000000000..b60ee95c3d8 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorPagingTests.Create_ProjectsOneProperty_Edges__sql.snap @@ -0,0 +1,2 @@ +SELECT "d"."Baz" +FROM "Data" AS "d" diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorPagingTests.Create_ProjectsOneProperty_Nodes.snap b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorPagingTests.Create_ProjectsOneProperty_Nodes.snap new file mode 100644 index 00000000000..193b9985a2d --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorPagingTests.Create_ProjectsOneProperty_Nodes.snap @@ -0,0 +1,14 @@ +{ + "data": { + "root": { + "nodes": [ + { + "baz": "a" + }, + { + "baz": "b" + } + ] + } + } +} diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorPagingTests.Create_ProjectsOneProperty_Nodes__sql.snap b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorPagingTests.Create_ProjectsOneProperty_Nodes__sql.snap new file mode 100644 index 00000000000..b60ee95c3d8 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorPagingTests.Create_ProjectsOneProperty_Nodes__sql.snap @@ -0,0 +1,2 @@ +SELECT "d"."Baz" +FROM "Data" AS "d" diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorPagingTests.Create_ProjectsTwoProperties_Edges.snap b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorPagingTests.Create_ProjectsTwoProperties_Edges.snap new file mode 100644 index 00000000000..3ed912fbe5c --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorPagingTests.Create_ProjectsTwoProperties_Edges.snap @@ -0,0 +1,20 @@ +{ + "data": { + "root": { + "edges": [ + { + "node": { + "bar": true, + "baz": "a" + } + }, + { + "node": { + "bar": false, + "baz": "b" + } + } + ] + } + } +} diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorPagingTests.Create_ProjectsTwoProperties_EdgesAndNodes.snap b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorPagingTests.Create_ProjectsTwoProperties_EdgesAndNodes.snap new file mode 100644 index 00000000000..f3fa8fa7971 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorPagingTests.Create_ProjectsTwoProperties_EdgesAndNodes.snap @@ -0,0 +1,28 @@ +{ + "data": { + "root": { + "nodes": [ + { + "bar": true, + "baz": "a" + }, + { + "bar": false, + "baz": "b" + } + ], + "edges": [ + { + "node": { + "bar": true + } + }, + { + "node": { + "bar": false + } + } + ] + } + } +} diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorPagingTests.Create_ProjectsTwoProperties_EdgesAndNodes__sql.snap b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorPagingTests.Create_ProjectsTwoProperties_EdgesAndNodes__sql.snap new file mode 100644 index 00000000000..264538e12db --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorPagingTests.Create_ProjectsTwoProperties_EdgesAndNodes__sql.snap @@ -0,0 +1,2 @@ +SELECT "d"."Bar", "d"."Baz" +FROM "Data" AS "d" diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorPagingTests.Create_ProjectsTwoProperties_Edges__sql.snap b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorPagingTests.Create_ProjectsTwoProperties_Edges__sql.snap new file mode 100644 index 00000000000..264538e12db --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorPagingTests.Create_ProjectsTwoProperties_Edges__sql.snap @@ -0,0 +1,2 @@ +SELECT "d"."Bar", "d"."Baz" +FROM "Data" AS "d" diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorPagingTests.Create_ProjectsTwoProperties_Nodes.snap b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorPagingTests.Create_ProjectsTwoProperties_Nodes.snap new file mode 100644 index 00000000000..30f9d1c65b7 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorPagingTests.Create_ProjectsTwoProperties_Nodes.snap @@ -0,0 +1,16 @@ +{ + "data": { + "root": { + "nodes": [ + { + "bar": true, + "baz": "a" + }, + { + "bar": false, + "baz": "b" + } + ] + } + } +} diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorPagingTests.Create_ProjectsTwoProperties_Nodes__sql.snap b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorPagingTests.Create_ProjectsTwoProperties_Nodes__sql.snap new file mode 100644 index 00000000000..264538e12db --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorPagingTests.Create_ProjectsTwoProperties_Nodes__sql.snap @@ -0,0 +1,2 @@ +SELECT "d"."Bar", "d"."Baz" +FROM "Data" AS "d" diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorScalarTests.Create_ProjectsOneProperty_Expression.snap b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorScalarTests.Create_ProjectsOneProperty_Expression.snap new file mode 100644 index 00000000000..66f87a69e6e --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorScalarTests.Create_ProjectsOneProperty_Expression.snap @@ -0,0 +1,12 @@ +{ + "data": { + "root": [ + { + "baz": "a" + }, + { + "baz": "b" + } + ] + } +} diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorScalarTests.Create_ProjectsOneProperty_Expression__sql.snap b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorScalarTests.Create_ProjectsOneProperty_Expression__sql.snap new file mode 100644 index 00000000000..b60ee95c3d8 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorScalarTests.Create_ProjectsOneProperty_Expression__sql.snap @@ -0,0 +1,2 @@ +SELECT "d"."Baz" +FROM "Data" AS "d" diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorScalarTests.Create_ProjectsOneProperty_WithResolver.snap b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorScalarTests.Create_ProjectsOneProperty_WithResolver.snap new file mode 100644 index 00000000000..0c1b3be21a1 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorScalarTests.Create_ProjectsOneProperty_WithResolver.snap @@ -0,0 +1,18 @@ +{ + "data": { + "root": [ + { + "baz": "a", + "foo": [ + "foo" + ] + }, + { + "baz": "b", + "foo": [ + "foo" + ] + } + ] + } +} diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorScalarTests.Create_ProjectsOneProperty_WithResolver__sql.snap b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorScalarTests.Create_ProjectsOneProperty_WithResolver__sql.snap new file mode 100644 index 00000000000..b60ee95c3d8 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorScalarTests.Create_ProjectsOneProperty_WithResolver__sql.snap @@ -0,0 +1,2 @@ +SELECT "d"."Baz" +FROM "Data" AS "d" diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorScalarTests.Create_ProjectsTwoProperties_Expression.snap b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorScalarTests.Create_ProjectsTwoProperties_Expression.snap new file mode 100644 index 00000000000..765ccf68d9f --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorScalarTests.Create_ProjectsTwoProperties_Expression.snap @@ -0,0 +1,14 @@ +{ + "data": { + "root": [ + { + "bar": true, + "baz": "a" + }, + { + "bar": false, + "baz": "b" + } + ] + } +} diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorScalarTests.Create_ProjectsTwoProperties_Expression__sql.snap b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorScalarTests.Create_ProjectsTwoProperties_Expression__sql.snap new file mode 100644 index 00000000000..264538e12db --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableProjectionVisitorScalarTests.Create_ProjectsTwoProperties_Expression__sql.snap @@ -0,0 +1,2 @@ +SELECT "d"."Bar", "d"."Baz" +FROM "Data" AS "d" diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableSingleOrDefaultTests.Create_DeepFilterObjectTwoProjections.snap b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableSingleOrDefaultTests.Create_DeepFilterObjectTwoProjections.snap new file mode 100644 index 00000000000..54349513174 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableSingleOrDefaultTests.Create_DeepFilterObjectTwoProjections.snap @@ -0,0 +1,19 @@ +{ + "errors": [ + { + "message": "Unexpected Execution Error", + "locations": [ + { + "line": 3, + "column": 29 + } + ], + "path": [ + "root" + ] + } + ], + "data": { + "root": null + } +} diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableSingleOrDefaultTests.Create_DeepFilterObjectTwoProjections_Nullable.snap b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableSingleOrDefaultTests.Create_DeepFilterObjectTwoProjections_Nullable.snap new file mode 100644 index 00000000000..54349513174 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableSingleOrDefaultTests.Create_DeepFilterObjectTwoProjections_Nullable.snap @@ -0,0 +1,19 @@ +{ + "errors": [ + { + "message": "Unexpected Execution Error", + "locations": [ + { + "line": 3, + "column": 29 + } + ], + "path": [ + "root" + ] + } + ], + "data": { + "root": null + } +} diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableSingleOrDefaultTests.Create_DeepFilterObjectTwoProjections_Nullable__sql.snap b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableSingleOrDefaultTests.Create_DeepFilterObjectTwoProjections_Nullable__sql.snap new file mode 100644 index 00000000000..85c5af4a707 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableSingleOrDefaultTests.Create_DeepFilterObjectTwoProjections_Nullable__sql.snap @@ -0,0 +1 @@ +Translating this query requires APPLY operation which is not supported on SQLite. diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableSingleOrDefaultTests.Create_DeepFilterObjectTwoProjections__sql.snap b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableSingleOrDefaultTests.Create_DeepFilterObjectTwoProjections__sql.snap new file mode 100644 index 00000000000..85c5af4a707 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableSingleOrDefaultTests.Create_DeepFilterObjectTwoProjections__sql.snap @@ -0,0 +1 @@ +Translating this query requires APPLY operation which is not supported on SQLite. diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableSingleOrDefaultTests.Create_ListObjectDifferentLevelProjection.snap b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableSingleOrDefaultTests.Create_ListObjectDifferentLevelProjection.snap new file mode 100644 index 00000000000..54349513174 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableSingleOrDefaultTests.Create_ListObjectDifferentLevelProjection.snap @@ -0,0 +1,19 @@ +{ + "errors": [ + { + "message": "Unexpected Execution Error", + "locations": [ + { + "line": 3, + "column": 29 + } + ], + "path": [ + "root" + ] + } + ], + "data": { + "root": null + } +} diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableSingleOrDefaultTests.Create_ListObjectDifferentLevelProjection_Nullable.snap b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableSingleOrDefaultTests.Create_ListObjectDifferentLevelProjection_Nullable.snap new file mode 100644 index 00000000000..54349513174 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableSingleOrDefaultTests.Create_ListObjectDifferentLevelProjection_Nullable.snap @@ -0,0 +1,19 @@ +{ + "errors": [ + { + "message": "Unexpected Execution Error", + "locations": [ + { + "line": 3, + "column": 29 + } + ], + "path": [ + "root" + ] + } + ], + "data": { + "root": null + } +} diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableSingleOrDefaultTests.Create_ListObjectDifferentLevelProjection_Nullable__sql.snap b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableSingleOrDefaultTests.Create_ListObjectDifferentLevelProjection_Nullable__sql.snap new file mode 100644 index 00000000000..85c5af4a707 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableSingleOrDefaultTests.Create_ListObjectDifferentLevelProjection_Nullable__sql.snap @@ -0,0 +1 @@ +Translating this query requires APPLY operation which is not supported on SQLite. diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableSingleOrDefaultTests.Create_ListObjectDifferentLevelProjection__sql.snap b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableSingleOrDefaultTests.Create_ListObjectDifferentLevelProjection__sql.snap new file mode 100644 index 00000000000..85c5af4a707 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableSingleOrDefaultTests.Create_ListObjectDifferentLevelProjection__sql.snap @@ -0,0 +1 @@ +Translating this query requires APPLY operation which is not supported on SQLite. diff --git a/src/HotChocolate/Data/test/Directory.Build.props b/src/HotChocolate/Data/test/Directory.Build.props index 25f5332857d..9ac2c13fd24 100644 --- a/src/HotChocolate/Data/test/Directory.Build.props +++ b/src/HotChocolate/Data/test/Directory.Build.props @@ -1,5 +1,5 @@ - + $(TestTargetFrameworks) @@ -17,12 +17,12 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - - - - - - + + + + + +