diff --git a/src/HotChocolate/Core/src/Execution/DependencyInjection/InternalSchemaServiceCollectionExtensions.cs b/src/HotChocolate/Core/src/Execution/DependencyInjection/InternalSchemaServiceCollectionExtensions.cs index e6b55a48d2b..b482e46ddaa 100644 --- a/src/HotChocolate/Core/src/Execution/DependencyInjection/InternalSchemaServiceCollectionExtensions.cs +++ b/src/HotChocolate/Core/src/Execution/DependencyInjection/InternalSchemaServiceCollectionExtensions.cs @@ -39,7 +39,7 @@ internal static IServiceCollection TryAddDiagnosticEvents( }); return services; } - + public static T GetApplicationService(this IServiceProvider services) where T : notnull => services.GetApplicationServices().GetRequiredService(); diff --git a/src/HotChocolate/Core/src/Execution/DependencyInjection/RequestExecutorServiceCollectionExtensions.cs b/src/HotChocolate/Core/src/Execution/DependencyInjection/RequestExecutorServiceCollectionExtensions.cs index 6e64ef194cf..0674fffdd03 100644 --- a/src/HotChocolate/Core/src/Execution/DependencyInjection/RequestExecutorServiceCollectionExtensions.cs +++ b/src/HotChocolate/Core/src/Execution/DependencyInjection/RequestExecutorServiceCollectionExtensions.cs @@ -9,6 +9,7 @@ using HotChocolate.Execution.Options; using HotChocolate.Execution.Processing; using HotChocolate.Fetching; +using HotChocolate.Internal; using HotChocolate.Language; using HotChocolate.Resolvers; using Microsoft.Extensions.DependencyInjection.Extensions; @@ -38,6 +39,7 @@ public static IServiceCollection AddGraphQLCore(this IServiceCollection services services.TryAddSingleton(); services.TryAddSingleton(sp => sp.GetRequiredService()); services.TryAddSingleton(); + services.TryAddSingleton(); services.TryAddSingleton>(sp => { diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Generators/DataLoaderSyntaxGenerator.cs b/src/HotChocolate/Core/src/Types.Analyzers/Generators/DataLoaderSyntaxGenerator.cs index 9187461d45e..4b6df65d077 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/Generators/DataLoaderSyntaxGenerator.cs +++ b/src/HotChocolate/Core/src/Types.Analyzers/Generators/DataLoaderSyntaxGenerator.cs @@ -18,7 +18,7 @@ public DataLoaderSyntaxGenerator() _writer = new CodeWriter(_sb); } - public void WriterHeader() + public void WriteHeader() { _writer.WriteFileHeader(); _writer.WriteIndentedLine("using Microsoft.Extensions.DependencyInjection;"); @@ -91,7 +91,7 @@ public void WriteBeginDataLoaderClass( key.ToFullyQualified(), value.ToFullyQualified()); break; - + case DataLoaderKind.Group: _writer.WriteIndentedLine( ": global::GreenDonut.GroupedDataLoader<{0}, {1}>", @@ -298,4 +298,4 @@ public void Dispose() _writer = default!; _disposed = true; } -} \ No newline at end of file +} diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Generators/ModuleSyntaxGenerator.cs b/src/HotChocolate/Core/src/Types.Analyzers/Generators/ModuleSyntaxGenerator.cs index c5197b21153..3c0a58fbeac 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/Generators/ModuleSyntaxGenerator.cs +++ b/src/HotChocolate/Core/src/Types.Analyzers/Generators/ModuleSyntaxGenerator.cs @@ -20,7 +20,7 @@ public ModuleSyntaxGenerator(string moduleName, string ns) _writer = new CodeWriter(_sb); } - public void WriterHeader() + public void WriteHeader() { _writer.WriteFileHeader(); _writer.WriteLine(); diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Generators/ObjectTypeExtensionSyntaxGenerator.cs b/src/HotChocolate/Core/src/Types.Analyzers/Generators/ObjectTypeExtensionSyntaxGenerator.cs index a38ebcc2cc4..014ef3bd959 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/Generators/ObjectTypeExtensionSyntaxGenerator.cs +++ b/src/HotChocolate/Core/src/Types.Analyzers/Generators/ObjectTypeExtensionSyntaxGenerator.cs @@ -1,29 +1,24 @@ using System.Text; using HotChocolate.Types.Analyzers.Helpers; using HotChocolate.Types.Analyzers.Inspectors; +using Microsoft.CodeAnalysis; namespace HotChocolate.Types.Analyzers.Generators; -public sealed class ObjectTypeExtensionSyntaxGenerator +public sealed class ObjectTypeExtensionSyntaxGenerator(StringBuilder sb, string ns) { - private readonly string _ns; - private readonly CodeWriter _writer; + private readonly CodeWriter _writer = new(sb); - public ObjectTypeExtensionSyntaxGenerator(StringBuilder sb, string ns) - { - _ns = ns; - _writer = new(sb); - } - - public void WriterHeader() + public void WriteHeader() { _writer.WriteFileHeader(); + _writer.WriteIndentedLine("using Microsoft.Extensions.DependencyInjection;"); _writer.WriteLine(); } public void WriteBeginNamespace() { - _writer.WriteIndentedLine("namespace {0}", _ns); + _writer.WriteIndentedLine("namespace {0}", ns); _writer.WriteIndentedLine("{"); _writer.IncreaseIndent(); } @@ -58,14 +53,17 @@ public void WriteInitializeMethod(ObjectTypeExtensionInfo objectTypeExtension) using (_writer.IncreaseIndent()) { - _writer.WriteIndentedLine("const global::System.Reflection.BindingFlags bindingFlags ="); - using (_writer.IncreaseIndent()) + if (objectTypeExtension.Members.Length > 0) { - _writer.WriteIndentedLine("global::System.Reflection.BindingFlags.Public |"); + _writer.WriteIndentedLine("const global::{0} bindingFlags =", WellKnownTypes.BindingFlags); using (_writer.IncreaseIndent()) { - _writer.WriteIndentedLine("System.Reflection.BindingFlags.NonPublic |"); - _writer.WriteIndentedLine("System.Reflection.BindingFlags.Static;"); + _writer.WriteIndentedLine("global::{0}.Public", WellKnownTypes.BindingFlags); + using (_writer.IncreaseIndent()) + { + _writer.WriteIndentedLine("| global::{0}.NonPublic", WellKnownTypes.BindingFlags); + _writer.WriteIndentedLine("| global::{0}.Static;", WellKnownTypes.BindingFlags); + } } } @@ -89,17 +87,62 @@ public void WriteInitializeMethod(ObjectTypeExtensionInfo objectTypeExtension) if (objectTypeExtension.Members.Length > 0) { - _writer.WriteLine(); foreach (var member in objectTypeExtension.Members) { - _writer.WriteIndentedLine( - "descriptor.Field(thisType.GetMember(\"{0}\", bindingFlags)[0]);", - member.Name); + _writer.WriteLine(); + _writer.WriteIndentedLine("descriptor"); + + using (_writer.IncreaseIndent()) + { + _writer.WriteIndentedLine( + ".Field(thisType.GetMember(\"{0}\", bindingFlags)[0])", + member.Name); + + if (member is IMethodSymbol method && + method.GetResultKind() is not ResolverResultKind.Pure) + { + _writer.WriteIndentedLine( + ".Extend().Definition.Resolver = {0}Resolvers.{1}_{2};", + objectTypeExtension.Type.ToDisplayString(), + objectTypeExtension.Type.Name, + member.Name); + } + else + { + _writer.WriteIndentedLine( + ".Extend().Definition.PureResolver = {0}Resolvers.{1}_{2};", + objectTypeExtension.Type.ToDisplayString(), + objectTypeExtension.Type.Name, + member.Name); + } + } } } _writer.WriteLine(); _writer.WriteIndentedLine("Configure(descriptor);"); + + if (objectTypeExtension.Members.Length > 0) + { + _writer.WriteLine(); + _writer.WriteIndentedLine("descriptor.Extend().Context.OnSchemaCreated("); + using (_writer.IncreaseIndent()) + { + _writer.WriteIndentedLine("schema =>"); + _writer.WriteIndentedLine("{"); + using (_writer.IncreaseIndent()) + { + _writer.WriteIndentedLine("var services = schema.Services.GetApplicationServices();"); + _writer.WriteIndentedLine( + "var bindingResolver = services.GetRequiredService();", + WellKnownTypes.ParameterBindingResolver); + _writer.WriteIndentedLine( + "global::{0}Resolvers.InitializeBindings(bindingResolver);", + objectTypeExtension.Type.ToDisplayString()); + } + _writer.WriteIndentedLine("});"); + } + } } _writer.WriteIndentedLine("}"); diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Generators/OperationFieldSyntaxGenerator.cs b/src/HotChocolate/Core/src/Types.Analyzers/Generators/OperationFieldSyntaxGenerator.cs index 4fb8b31add0..a9f9093e826 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/Generators/OperationFieldSyntaxGenerator.cs +++ b/src/HotChocolate/Core/src/Types.Analyzers/Generators/OperationFieldSyntaxGenerator.cs @@ -18,7 +18,7 @@ public OperationFieldSyntaxGenerator() _writer = new CodeWriter(_sb); } - public void WriterHeader() + public void WriteHeader() { _writer.WriteFileHeader(); _writer.WriteLine(); diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Generators/RequestMiddlewareSyntaxGenerator.cs b/src/HotChocolate/Core/src/Types.Analyzers/Generators/RequestMiddlewareSyntaxGenerator.cs index ba8091ef5b8..46e4169bf28 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/Generators/RequestMiddlewareSyntaxGenerator.cs +++ b/src/HotChocolate/Core/src/Types.Analyzers/Generators/RequestMiddlewareSyntaxGenerator.cs @@ -25,7 +25,7 @@ public RequestMiddlewareSyntaxGenerator(string moduleName, string ns) _writer = new(_sb); } - public void WriterHeader() + public void WriteHeader() { _writer.WriteFileHeader(); _writer.WriteIndentedLine("using Microsoft.Extensions.DependencyInjection;"); diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Generators/ResolverName.cs b/src/HotChocolate/Core/src/Types.Analyzers/Generators/ResolverName.cs new file mode 100644 index 00000000000..9e5cf671592 --- /dev/null +++ b/src/HotChocolate/Core/src/Types.Analyzers/Generators/ResolverName.cs @@ -0,0 +1,23 @@ +using Microsoft.CodeAnalysis; + +namespace HotChocolate.Types.Analyzers.Generators; + +public readonly struct ResolverName(string typeName, string memberName) +{ + public readonly string TypeName = typeName; + + public readonly string MemberName = memberName; +} + +public readonly struct ResolverInfo(ResolverName resolverName, IMethodSymbol? methodSymbol) +{ + public readonly ResolverName Name = resolverName; + + public readonly IMethodSymbol? Method = methodSymbol; + + public readonly int ParameterCount = methodSymbol?.Parameters.Length ?? 0; + + public readonly bool Skip => + ParameterCount == 0 || + (ParameterCount == 1 && (Method?.Parameters[0]?.IsParent() ?? false)); +} diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Generators/ResolverResultKind.cs b/src/HotChocolate/Core/src/Types.Analyzers/Generators/ResolverResultKind.cs new file mode 100644 index 00000000000..77b4b504d12 --- /dev/null +++ b/src/HotChocolate/Core/src/Types.Analyzers/Generators/ResolverResultKind.cs @@ -0,0 +1,12 @@ +namespace HotChocolate.Types.Analyzers.Generators; + +public enum ResolverResultKind +{ + Task, + Executable, + Queryable, + AsyncEnumerable, + TaskAsyncEnumerable, + Pure, + Invalid +} diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Generators/ResolverSyntaxGenerator.cs b/src/HotChocolate/Core/src/Types.Analyzers/Generators/ResolverSyntaxGenerator.cs new file mode 100644 index 00000000000..16d9fdf17f9 --- /dev/null +++ b/src/HotChocolate/Core/src/Types.Analyzers/Generators/ResolverSyntaxGenerator.cs @@ -0,0 +1,498 @@ +using System.Text; +using HotChocolate.Types.Analyzers.Helpers; +using Microsoft.CodeAnalysis; + +namespace HotChocolate.Types.Analyzers.Generators; + +public sealed class ResolverSyntaxGenerator(StringBuilder sb) +{ + private readonly CodeWriter _writer = new(sb); + + public void WriteHeader() + { + _writer.WriteFileHeader(); + _writer.WriteIndentedLine("using HotChocolate.Internal;"); + _writer.WriteLine(); + } + + public void WriteBeginNamespace(string ns) + { + _writer.WriteIndentedLine("namespace {0}", ns); + _writer.WriteIndentedLine("{"); + _writer.IncreaseIndent(); + } + + public void WriteEndNamespace() + { + _writer.DecreaseIndent(); + _writer.WriteIndentedLine("}"); + _writer.WriteLine(); + } + + public string WriteBeginClass(string typeName) + { + _writer.WriteIndentedLine("internal static class {0}", typeName); + _writer.WriteIndentedLine("{"); + _writer.IncreaseIndent(); + _writer.WriteIndentedLine("private static bool _bindingsInitialized;"); + return typeName; + } + + public void WriteEndClass() + { + _writer.DecreaseIndent(); + _writer.WriteIndentedLine("}"); + } + + public bool AddResolverDeclarations(IEnumerable resolvers) + { + var first = true; + + foreach (var resolver in resolvers) + { + if (resolver.Skip) + { + continue; + } + + if (!first) + { + _writer.WriteLine(); + } + first = false; + + _writer.WriteIndentedLine( + "private readonly static global::{0}[] _args_{1}_{2} = new global::{0}[{3}];", + WellKnownTypes.ParameterBinding, + resolver.Name.TypeName, + resolver.Name.MemberName, + resolver.ParameterCount); + } + + return !first; + } + + public void AddParameterInitializer(IEnumerable resolvers) + { + _writer.WriteIndentedLine( + "public static void InitializeBindings(global::{0} bindingResolver)", + WellKnownTypes.ParameterBindingResolver); + _writer.WriteIndentedLine("{"); + using (_writer.IncreaseIndent()) + { + var first = true; + foreach (var resolver in resolvers) + { + if (!resolver.Skip && resolver.Method is not null) + { + if (first) + { + _writer.WriteIndentedLine("if (_bindingsInitialized)"); + _writer.WriteIndentedLine("{"); + using (_writer.IncreaseIndent()) + { + _writer.WriteIndentedLine("return;"); + } + _writer.WriteIndentedLine("}"); + _writer.WriteIndentedLine("_bindingsInitialized = true;"); + _writer.WriteLine(); + _writer.WriteIndentedLine( + "const global::{0} bindingFlags =", + WellKnownTypes.BindingFlags); + _writer.WriteIndentedLine( + " global::{0}.Public", + WellKnownTypes.BindingFlags); + _writer.WriteIndentedLine( + " | global::{0}.NonPublic", + WellKnownTypes.BindingFlags); + _writer.WriteIndentedLine( + " | global::{0}.Static;", + WellKnownTypes.BindingFlags); + _writer.WriteIndentedLine( + "var type = typeof(global::{0});", + resolver.Method.ContainingType.ToDisplayString()); + first = false; + } + + _writer.WriteLine(); + _writer.WriteIndentedLine( + "var resolver_{0}_{1} = type.GetMethod(\"{1}\", bindingFlags, [{2}])!;", + resolver.Name.TypeName, + resolver.Name.MemberName, + string.Join(", ", resolver.Method.Parameters.Select( + p => $"typeof(global::{p.Type.ToDisplayString()})"))); + + _writer.WriteIndentedLine( + "var parameters_{0}_{1} = resolver_{0}_{1}.GetParameters();", + resolver.Name.TypeName, + resolver.Name.MemberName, + string.Join(", ", resolver.Method.Parameters.Select(p => $"typeof({p.Type.ToDisplayString()})"))); + + for (var i = 0; i < resolver.Method.Parameters.Length; i++) + { + _writer.WriteIndentedLine( + "_args_{0}_{1}[{2}] = bindingResolver.GetBinding(parameters_{0}_{1}[{2}]);", + resolver.Name.TypeName, + resolver.Name.MemberName, + i); + } + } + } + } + _writer.WriteIndentedLine("}"); + } + + public void AddResolver(ResolverName resolverName, ISymbol member, Compilation compilation) + { + if (member is IMethodSymbol method) + { + switch (method.GetResultKind()) + { + case ResolverResultKind.Invalid: + return; + + case ResolverResultKind.Pure: + AddStaticPureResolver(resolverName, method, compilation); + return; + + case ResolverResultKind.Task: + case ResolverResultKind.TaskAsyncEnumerable: + AddStaticStandardResolver(resolverName, method, true, compilation); + return; + + case ResolverResultKind.Executable: + case ResolverResultKind.Queryable: + case ResolverResultKind.AsyncEnumerable: + AddStaticStandardResolver(resolverName, method, false, compilation); + return; + } + } + + AddStaticPropertyResolver(resolverName, member); + } + + private void AddStaticStandardResolver( + ResolverName resolverName, + IMethodSymbol method, + bool async, + Compilation compilation) + { + _writer.WriteIndented("public static "); + + if (async) + { + _writer.Write("async "); + } + + _writer.WriteLine( + "global::{0}<{1}?> {2}_{3}(global::{4} context)", + WellKnownTypes.ValueTask, + WellKnownTypes.Object, + resolverName.TypeName, + resolverName.MemberName, + WellKnownTypes.ResolverContext); + _writer.WriteIndentedLine("{"); + using (_writer.IncreaseIndent()) + { + AddResolverArguments(resolverName, method, compilation); + + if (async) + { + _writer.WriteIndentedLine( + "var result = await {0}.{1}({2});", + method.ContainingType.ToFullyQualified(), + resolverName.MemberName, + GetResolverArguments(method)); + + _writer.WriteIndentedLine( + "return result;", + WellKnownTypes.ValueTask, + WellKnownTypes.Object); + } + else + { + _writer.WriteIndentedLine( + "var result = {0}.{1}({2});", + method.ContainingType.ToFullyQualified(), + resolverName.MemberName, + GetResolverArguments(method)); + + _writer.WriteIndentedLine( + "return new global::{0}<{1}?>(result);", + WellKnownTypes.ValueTask, + WellKnownTypes.Object); + } + } + _writer.WriteIndentedLine("}"); + } + + private void AddStaticPureResolver(ResolverName resolverName, IMethodSymbol method, Compilation compilation) + { + _writer.WriteIndentedLine( + "public static {0}? {1}_{2}(global::{3} context)", + WellKnownTypes.Object, + resolverName.TypeName, + resolverName.MemberName, + WellKnownTypes.PureResolverContext); + _writer.WriteIndentedLine("{"); + using (_writer.IncreaseIndent()) + { + AddResolverArguments(resolverName, method, compilation); + + _writer.WriteIndentedLine( + "var result = {0}.{1}({2});", + method.ContainingType.ToFullyQualified(), + resolverName.MemberName, + GetResolverArguments(method)); + + _writer.WriteIndentedLine("return result;"); + } + _writer.WriteIndentedLine("}"); + } + + private void AddStaticPropertyResolver(ResolverName resolverName, ISymbol method) + { + _writer.WriteIndentedLine( + "public static {0}? {1}_{2}(global::{3} context)", + WellKnownTypes.Object, + resolverName.TypeName, + resolverName.MemberName, + WellKnownTypes.PureResolverContext); + _writer.WriteIndentedLine("{"); + using (_writer.IncreaseIndent()) + { + _writer.WriteIndentedLine( + "var result = {0}.{1};", + method.ContainingType.ToFullyQualified(), + resolverName.MemberName); + + _writer.WriteIndentedLine( + "return result;", + WellKnownTypes.ValueTask, + WellKnownTypes.Object); + } + _writer.WriteIndentedLine("}"); + } + + private void AddResolverArguments(ResolverName resolverName, IMethodSymbol method, Compilation compilation) + { + if (method.Parameters.Length > 0) + { + for (var i = 0; i < method.Parameters.Length; i++) + { + var parameter = method.Parameters[i]; + + if (parameter.IsParent()) + { + _writer.WriteIndentedLine( + "var args{0} = context.Parent<{1}>();", + i, + method.Parameters[i].Type.ToFullyQualified()); + } + else if (parameter.IsCancellationToken()) + { + _writer.WriteIndentedLine("var args{0} = context.RequestAborted;", i); + } + else if (parameter.IsClaimsPrincipal()) + { + _writer.WriteIndentedLine( + "var args{0} = context.GetGlobalState<{1}>(\"ClaimsPrincipal\");", + i, + WellKnownTypes.ClaimsPrincipal); + } + else if (parameter.IsDocumentNode()) + { + _writer.WriteIndentedLine("var args{0} = context.Operation.Document;", i); + } + else if (parameter.IsEventMessage()) + { + _writer.WriteIndentedLine( + "var args{0} = context.GetGlobalState<{1}>(" + + "global::HotChocolate.WellKnownContextData.EventMessage)!;", + i, + parameter.Type.ToFullyQualified()); + } + else if (parameter.IsFieldNode()) + { + _writer.WriteIndentedLine( + "var args{0} = context.Selection.SyntaxNode", + i, + parameter.Type.ToFullyQualified()); + } + else if (parameter.IsOutputField(compilation)) + { + _writer.WriteIndentedLine( + "var args{0} = context.Selection.Field", + i, + parameter.Type.ToFullyQualified()); + } + else if (parameter.IsGlobalState(out var key)) + { + if (parameter.HasExplicitDefaultValue) + { + var defaultValue = parameter.ExplicitDefaultValue; + + var defaultValueString = ConvertDefaultValueToString(defaultValue, parameter.Type); + + _writer.WriteIndentedLine( + "var args{0} = context.GetGlobalStateOrDefault<{1}>({2}, {3})!;", + i, + parameter.Type.ToFullyQualified(), + key, + defaultValueString); + } + else if (parameter.IsNonNullable()) + { + _writer.WriteIndentedLine( + "var args{0} = context.GetGlobalState<{1}>({2})!;", + i, + parameter.Type.ToFullyQualified(), + key); + } + else + { + _writer.WriteIndentedLine( + "var args{0} = context.GetGlobalStateOrDefault<{1}>({2})!;", + i, + parameter.Type.ToFullyQualified(), + key); + } + } + else if (parameter.IsScopedState(out key)) + { + if (parameter.HasExplicitDefaultValue) + { + var defaultValue = parameter.ExplicitDefaultValue; + + var defaultValueString = ConvertDefaultValueToString(defaultValue, parameter.Type); + + _writer.WriteIndentedLine( + "var args{0} = context.GetScopedStateOrDefault<{1}>({2}, {3})!;", + i, + parameter.Type.ToFullyQualified(), + key, + defaultValueString); + } + else if (parameter.IsNonNullable()) + { + _writer.WriteIndentedLine( + "var args{0} = context.GetScopedState<{1}>({2})!;", + i, + parameter.Type.ToFullyQualified(), + key); + } + else + { + _writer.WriteIndentedLine( + "var args{0} = context.GetScopedStateOrDefault<{1}>({2})!;", + i, + parameter.Type.ToFullyQualified(), + key); + } + } + else if (parameter.IsLocalState(out key)) + { + if (parameter.HasExplicitDefaultValue) + { + var defaultValue = parameter.ExplicitDefaultValue; + + var defaultValueString = ConvertDefaultValueToString(defaultValue, parameter.Type); + + _writer.WriteIndentedLine( + "var args{0} = context.GetLocalStateOrDefault<{1}>({2}, {3})!;", + i, + parameter.Type.ToFullyQualified(), + key, + defaultValueString); + } + else if (parameter.IsNonNullable()) + { + _writer.WriteIndentedLine( + "var args{0} = context.GetLocalState<{1}>({2})!;", + i, + parameter.Type.ToFullyQualified(), + key); + } + else + { + _writer.WriteIndentedLine( + "var args{0} = context.GetLocalStateOrDefault<{1}>({2})!;", + i, + parameter.Type.ToFullyQualified(), + key); + } + } + else + { + _writer.WriteIndentedLine( + "var args{0} = _args_{1}_{2}[{0}].Execute<{3}>(context);", + i, + resolverName.TypeName, + resolverName.MemberName, + method.Parameters[i].Type.ToFullyQualified()); + } + } + } + } + + private string GetResolverArguments(IMethodSymbol method) + { + if (method.Parameters.Length == 0) + { + return string.Empty; + } + + var arguments = new StringBuilder(); + + for (var i = 0; i < method.Parameters.Length; i++) + { + if (i > 0) + { + arguments.Append(", "); + } + arguments.Append($"args{i}"); + } + + return arguments.ToString(); + } + + private static string ConvertDefaultValueToString(object? defaultValue, ITypeSymbol type) + { + if (defaultValue == null) + { + return "null"; + } + + if (type.SpecialType == SpecialType.System_String) + { + return $"\"{defaultValue}\""; + } + + if (type.SpecialType == SpecialType.System_Char) + { + return $"'{defaultValue}'"; + } + + if (type.SpecialType == SpecialType.System_Boolean) + { + return defaultValue.ToString().ToLower(); + } + + if (type.SpecialType == SpecialType.System_Double || type.SpecialType == SpecialType.System_Single) + { + return $"{defaultValue}d"; + } + + if (type.SpecialType == SpecialType.System_Decimal) + { + return $"{defaultValue}m"; + } + + if (type.SpecialType == SpecialType.System_Int64 || type.SpecialType == SpecialType.System_UInt64) + { + return $"{defaultValue}L"; + } + + return defaultValue.ToString(); + } +} diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Generators/SymbolExtensions.cs b/src/HotChocolate/Core/src/Types.Analyzers/Generators/SymbolExtensions.cs new file mode 100644 index 00000000000..241c85fd476 --- /dev/null +++ b/src/HotChocolate/Core/src/Types.Analyzers/Generators/SymbolExtensions.cs @@ -0,0 +1,212 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; + +namespace HotChocolate.Types.Analyzers.Generators; + +public static class SymbolExtensions +{ + public static bool IsParent(this IParameterSymbol parameter) + => parameter.IsThis || + parameter + .GetAttributes() + .Any(static t => t.AttributeClass?.ToDisplayString() == WellKnownAttributes.ParentAttribute); + + public static bool IsCancellationToken(this IParameterSymbol parameter) + => parameter.Type.ToDisplayString() == WellKnownTypes.CancellationToken; + + public static bool IsClaimsPrincipal(this IParameterSymbol parameter) + => parameter.Type.ToDisplayString() == WellKnownTypes.ClaimsPrincipal; + + public static bool IsDocumentNode(this IParameterSymbol parameter) + => parameter.Type.ToDisplayString() == WellKnownTypes.DocumentNode; + + public static bool IsEventMessage(this IParameterSymbol parameter) + => parameter.Type.ToDisplayString() == WellKnownAttributes.EnumTypeAttribute; + + public static bool IsFieldNode(this IParameterSymbol parameter) + => parameter.Type.ToDisplayString() == WellKnownAttributes.FieldNode; + + public static bool IsOutputField(this IParameterSymbol parameterSymbol, Compilation compilation) + { + var type = compilation.GetTypeByMetadataName(WellKnownTypes.OutputField); + + if (type == null) + { + return false; + } + + return compilation.ClassifyConversion(parameterSymbol.Type, type).IsImplicit; + } + + public static bool IsGlobalState( + this IParameterSymbol parameter, + [NotNullWhen(true)] out string? key) + { + key = null; + + foreach (var attributeData in parameter.GetAttributes()) + { + if (attributeData.AttributeClass?.ToDisplayString() == "HotChocolate.GlobalStateAttribute") + { + if (attributeData.ConstructorArguments.Length == 1 && + attributeData.ConstructorArguments[0].Kind == TypedConstantKind.Primitive && + attributeData.ConstructorArguments[0].Value is string keyValue) + { + key = keyValue; + return true; + } + + foreach (var namedArg in attributeData.NamedArguments) + { + if (namedArg.Key == "Key" && namedArg.Value.Value is string namedKeyValue) + { + key = namedKeyValue; + return true; + } + } + + key = parameter.Name; + return true; + } + } + + return false; + } + + public static bool IsScopedState( + this IParameterSymbol parameter, + [NotNullWhen(true)] out string? key) + { + key = null; + + foreach (var attributeData in parameter.GetAttributes()) + { + if (attributeData.AttributeClass?.ToDisplayString() == "HotChocolate.ScopedStateAttribute") + { + if (attributeData.ConstructorArguments.Length == 1 && + attributeData.ConstructorArguments[0].Kind == TypedConstantKind.Primitive && + attributeData.ConstructorArguments[0].Value is string keyValue) + { + key = keyValue; + return true; + } + + foreach (var namedArg in attributeData.NamedArguments) + { + if (namedArg.Key == "Key" && namedArg.Value.Value is string namedKeyValue) + { + key = namedKeyValue; + return true; + } + } + + key = parameter.Name; + return true; + } + } + + return false; + } + + public static bool IsLocalState( + this IParameterSymbol parameter, + [NotNullWhen(true)] out string? key) + { + key = null; + + foreach (var attributeData in parameter.GetAttributes()) + { + if (attributeData.AttributeClass?.ToDisplayString() == "HotChocolate.LocalStateAttribute") + { + if (attributeData.ConstructorArguments.Length == 1 && + attributeData.ConstructorArguments[0].Kind == TypedConstantKind.Primitive && + attributeData.ConstructorArguments[0].Value is string keyValue) + { + key = keyValue; + return true; + } + + foreach (var namedArg in attributeData.NamedArguments) + { + if (namedArg.Key == "Key" && namedArg.Value.Value is string namedKeyValue) + { + key = namedKeyValue; + return true; + } + } + + key = parameter.Name; + return true; + } + } + + return false; + } + + public static bool IsNonNullable(this IParameterSymbol parameter) + { + if (parameter.Type.NullableAnnotation != NullableAnnotation.NotAnnotated) + { + return false; + } + + if (parameter.Type is INamedTypeSymbol namedTypeSymbol && + namedTypeSymbol.ConstructedFrom.SpecialType == SpecialType.System_Nullable_T) + { + return false; + } + + return true; + } + + public static ResolverResultKind GetResultKind(this IMethodSymbol method) + { + const string task = $"{WellKnownTypes.Task}<"; + const string valueTask = $"{WellKnownTypes.ValueTask}<"; + const string taskEnumerable = $"{WellKnownTypes.Task}<{WellKnownTypes.AsyncEnumerable}<"; + const string valueTaskEnumerable = $"{WellKnownTypes.ValueTask}<{WellKnownTypes.AsyncEnumerable}<"; + + if (method.ReturnsVoid || method.ReturnsByRef || method.ReturnsByRefReadonly) + { + return ResolverResultKind.Invalid; + } + + var returnType = method.ReturnType.ToDisplayString(); + + if (returnType.Equals(WellKnownTypes.Task) || + returnType.Equals(WellKnownTypes.ValueTask)) + { + return ResolverResultKind.Invalid; + } + + if (returnType.StartsWith(task) || + returnType.StartsWith(valueTask)) + { + if (returnType.StartsWith(taskEnumerable) || + returnType.StartsWith(valueTaskEnumerable)) + { + return ResolverResultKind.TaskAsyncEnumerable; + } + + return ResolverResultKind.Task; + } + + if (returnType.StartsWith(WellKnownTypes.Executable)) + { + return ResolverResultKind.Executable; + } + + if (returnType.StartsWith(WellKnownTypes.Queryable)) + { + return ResolverResultKind.Queryable; + } + + if (returnType.StartsWith(WellKnownTypes.AsyncEnumerable)) + { + return ResolverResultKind.AsyncEnumerable; + } + + return ResolverResultKind.Pure; + } +} diff --git a/src/HotChocolate/Core/src/Types.Analyzers/MiddlewareGenerator.cs b/src/HotChocolate/Core/src/Types.Analyzers/MiddlewareGenerator.cs index 8a283e62046..08e342bd971 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/MiddlewareGenerator.cs +++ b/src/HotChocolate/Core/src/Types.Analyzers/MiddlewareGenerator.cs @@ -86,7 +86,7 @@ private static void Execute( using var generator = new RequestMiddlewareSyntaxGenerator(module.ModuleName, _namespace); - generator.WriterHeader(); + generator.WriteHeader(); generator.WriteBeginNamespace(); generator.WriteBeginClass(); diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Properties/SourceGenResources.Designer.cs b/src/HotChocolate/Core/src/Types.Analyzers/Properties/SourceGenResources.Designer.cs index 2bd644d46c3..d227ef48a04 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/Properties/SourceGenResources.Designer.cs +++ b/src/HotChocolate/Core/src/Types.Analyzers/Properties/SourceGenResources.Designer.cs @@ -9,21 +9,21 @@ namespace HotChocolate.Types.Analyzers.Properties { using System; - - + + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] [System.Diagnostics.DebuggerNonUserCodeAttribute()] [System.Runtime.CompilerServices.CompilerGeneratedAttribute()] internal class SourceGenResources { - + private static System.Resources.ResourceManager resourceMan; - + private static System.Globalization.CultureInfo resourceCulture; - + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] internal SourceGenResources() { } - + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] internal static System.Resources.ResourceManager ResourceManager { get { @@ -34,7 +34,7 @@ internal static System.Resources.ResourceManager ResourceManager { return resourceMan; } } - + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] internal static System.Globalization.CultureInfo Culture { get { @@ -44,19 +44,19 @@ internal static System.Globalization.CultureInfo Culture { resourceCulture = value; } } - + internal static string DataLoader_KeyParameterMissing { get { return ResourceManager.GetString("DataLoader_KeyParameterMissing", resourceCulture); } } - + internal static string DataLoader_InvalidAccessModifier { get { return ResourceManager.GetString("DataLoader_InvalidAccessModifier", resourceCulture); } } - + internal static string InterceptsAttribute { get { return ResourceManager.GetString("InterceptsAttribute", resourceCulture); diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Properties/SourceGenResources.resx b/src/HotChocolate/Core/src/Types.Analyzers/Properties/SourceGenResources.resx index 11067097b9e..43a4cff6b21 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/Properties/SourceGenResources.resx +++ b/src/HotChocolate/Core/src/Types.Analyzers/Properties/SourceGenResources.resx @@ -3,7 +3,7 @@ - + diff --git a/src/HotChocolate/Core/src/Types.Analyzers/TypeModuleGenerator.cs b/src/HotChocolate/Core/src/Types.Analyzers/TypeModuleGenerator.cs index 834a620a671..46df6265e81 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/TypeModuleGenerator.cs +++ b/src/HotChocolate/Core/src/Types.Analyzers/TypeModuleGenerator.cs @@ -107,7 +107,7 @@ private static void WriteConfiguration( { using var generator = new ModuleSyntaxGenerator(module.ModuleName, "Microsoft.Extensions.DependencyInjection"); - generator.WriterHeader(); + generator.WriteHeader(); generator.WriteBeginNamespace(); generator.WriteBeginClass(); generator.WriteBeginRegistrationMethod(); @@ -255,7 +255,7 @@ private static void WriteDataLoader( } using var generator = new DataLoaderSyntaxGenerator(); - generator.WriterHeader(); + generator.WriteHeader(); foreach (var group in dataLoaders.GroupBy(t => t.Namespace)) { @@ -335,7 +335,7 @@ private static void WriteOperationTypes( } using var generator = new OperationFieldSyntaxGenerator(); - generator.WriterHeader(); + generator.WriteHeader(); generator.WriteBeginNamespace("Microsoft.Extensions.DependencyInjection"); foreach (var group in operations.GroupBy(t => t.Type)) diff --git a/src/HotChocolate/Core/src/Types.Analyzers/TypesGenerator.cs b/src/HotChocolate/Core/src/Types.Analyzers/TypesGenerator.cs index 2e3a8d4d951..76e14bd29c4 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/TypesGenerator.cs +++ b/src/HotChocolate/Core/src/Types.Analyzers/TypesGenerator.cs @@ -1,4 +1,5 @@ using System.Collections.Immutable; +using System.Text; using HotChocolate.Types.Analyzers.Generators; using HotChocolate.Types.Analyzers.Helpers; using HotChocolate.Types.Analyzers.Inspectors; @@ -67,24 +68,37 @@ private static void Execute( } var sb = StringBuilderPool.Get(); - var first = true; + WriteTypes(context, syntaxInfos, sb); + + sb.Clear(); + + WriteResolvers(context, compilation, syntaxInfos, sb); + + StringBuilderPool.Return(sb); + } + + private static void WriteTypes( + SourceProductionContext context, + ImmutableArray syntaxInfos, + StringBuilder sb) + { + var firstNamespace = true; foreach (var group in syntaxInfos .OfType() .GroupBy(t => t.Type.ContainingNamespace.ToDisplayString())) { var generator = new ObjectTypeExtensionSyntaxGenerator(sb, group.Key); - if (first) + if (firstNamespace) { - generator.WriterHeader(); - first = false; + generator.WriteHeader(); + firstNamespace = false; } generator.WriteBeginNamespace(); var firstClass = true; - foreach (var objectTypeExtension in group) { if (objectTypeExtension.Diagnostics.Length > 0) @@ -114,6 +128,73 @@ private static void Execute( } context.AddSource(WellKnownFileNames.TypesFile, sb.ToString()); - StringBuilderPool.Return(sb); } + + private static void WriteResolvers( + SourceProductionContext context, + Compilation compilation, + ImmutableArray syntaxInfos, + StringBuilder sb) + { + var generator = new ResolverSyntaxGenerator(sb); + generator.WriteHeader(); + + var firstNamespace = true; + foreach (var group in syntaxInfos + .OfType() + .GroupBy(t => t.Type.ContainingNamespace.ToDisplayString())) + { + if(!firstNamespace) + { + sb.AppendLine(); + } + firstNamespace = false; + + generator.WriteBeginNamespace(group.Key); + + var firstClass = true; + foreach (var objectTypeExtension in group) + { + if(!firstClass) + { + sb.AppendLine(); + } + firstClass = false; + + var resolverInfos = objectTypeExtension.Members.Select( + m => CreateResolverInfo(objectTypeExtension, m)); + + generator.WriteBeginClass(objectTypeExtension.Type.Name + "Resolvers"); + + if (generator.AddResolverDeclarations(resolverInfos)) + { + sb.AppendLine(); + } + + generator.AddParameterInitializer(resolverInfos); + + foreach (var member in objectTypeExtension.Members) + { + sb.AppendLine(); + generator.AddResolver( + new ResolverName(objectTypeExtension.Type.Name, member.Name), + member, + compilation); + } + + generator.WriteEndClass(); + } + + generator.WriteEndNamespace(); + } + + context.AddSource(WellKnownFileNames.ResolversFile, sb.ToString()); + } + + private static ResolverInfo CreateResolverInfo( + ObjectTypeExtensionInfo objectTypeExtension, + ISymbol member) + => new ResolverInfo( + new ResolverName(objectTypeExtension.Type.Name, member.Name), + member as IMethodSymbol); } diff --git a/src/HotChocolate/Core/src/Types.Analyzers/WellKnownAttributes.cs b/src/HotChocolate/Core/src/Types.Analyzers/WellKnownAttributes.cs index 44a26c62198..b7b9a63ac22 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/WellKnownAttributes.cs +++ b/src/HotChocolate/Core/src/Types.Analyzers/WellKnownAttributes.cs @@ -16,6 +16,9 @@ public static class WellKnownAttributes public const string MutationAttribute = "HotChocolate.MutationAttribute"; public const string SubscriptionAttribute = "HotChocolate.SubscriptionAttribute"; public const string NodeResolverAttribute = "HotChocolate.Types.Relay.NodeResolverAttribute"; + public const string ParentAttribute = "HotChocolate.ParentAttribute"; + public const string EventMessageAttribute = "HotChocolate.EventMessageAttribute"; + public const string FieldNode = "HotChocolate.Language.FieldNode"; public static HashSet TypeAttributes { get; } = [ diff --git a/src/HotChocolate/Core/src/Types.Analyzers/WellKnownFileNames.cs b/src/HotChocolate/Core/src/Types.Analyzers/WellKnownFileNames.cs index bb1c59d2515..c82026ef72a 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/WellKnownFileNames.cs +++ b/src/HotChocolate/Core/src/Types.Analyzers/WellKnownFileNames.cs @@ -8,4 +8,5 @@ public static class WellKnownFileNames public const string RootTypesFile = "HotChocolateRootTypes.735550c.g.cs"; public const string MiddlewareFile = "HotChocolateMiddleware.735550c.g.cs"; public const string TypesFile = "HotChocolateTypes.735550c.g.cs"; + public const string ResolversFile = "HotChocolateResolvers.735550c.g.cs"; } diff --git a/src/HotChocolate/Core/src/Types.Analyzers/WellKnownTypes.cs b/src/HotChocolate/Core/src/Types.Analyzers/WellKnownTypes.cs index 173d0029b3d..462f0712287 100644 --- a/src/HotChocolate/Core/src/Types.Analyzers/WellKnownTypes.cs +++ b/src/HotChocolate/Core/src/Types.Analyzers/WellKnownTypes.cs @@ -1,3 +1,5 @@ +using System.Data; + namespace HotChocolate.Types.Analyzers; public static class WellKnownTypes @@ -20,12 +22,33 @@ public static class WellKnownTypes public const string CancellationToken = "System.Threading.CancellationToken"; public const string GlobalCancellationToken = "global::System.Threading.CancellationToken"; public const string ReadOnlyList = "System.Collections.Generic.IReadOnlyList"; + public const string AsyncEnumerable = "System.Collections.Generic.IAsyncEnumerable"; + public const string Queryable = "System.Linq.IQueryable"; + public const string Dictionary = "System.Collections.Generic.Dictionary"; public const string ReadOnlyDictionary = "System.Collections.Generic.IReadOnlyDictionary"; public const string Lookup = "System.Linq.ILookup"; public const string Task = "System.Threading.Tasks.Task"; - public const string RequestCoreMiddleware = $"HotChocolate.Execution.{nameof(RequestCoreMiddleware)}"; + public const string ValueTask = "System.Threading.Tasks.ValueTask"; + public const string RequestCoreMiddleware = $"HotChocolate.Execution.{nameof(RequestCoreMiddleware)}"; public const string Schema = $"HotChocolate.{nameof(Schema)}"; public const string RequestExecutorBuilder = "HotChocolate.Execution.Configuration.IRequestExecutorBuilder"; + public const string FieldResolverDelegate = "HotChocolate.Resolvers.FieldResolverDelegate"; + public const string ResolverContext = "HotChocolate.Resolvers.IResolverContext"; + public const string PureResolverContext = "HotChocolate.Resolvers.IPureResolverContext"; + public const string ParameterBinding = "HotChocolate.Internal.IParameterBinding"; + public const string MemoryMarshal = "System.Runtime.InteropServices.MemoryMarshal"; + public const string Unsafe = "System.Runtime.CompilerServices.Unsafe"; + public const string Object = "System.Object"; + public const string Executable = "HotChocolate.IExecutable"; + public const string ClaimsPrincipal = "System.Security.Claims.ClaimsPrincipal"; + public const string DocumentNode = "HotChocolate.Language.DocumentNode"; + public const string OutputField = "HotChocolate.Types.IOutputField"; + public const string ParameterBindingResolver = "HotChocolate.Internal.IParameterBindingResolver"; + public const string CustomAttributeData = "HotChocolate.Internal.GenCustomAttributeData"; + public const string ParameterInfo = "HotChocolate.Internal.GenParameterInfo"; + public const string CustomAttributeTypedArgument = "System.Reflection.CustomAttributeTypedArgument"; + public const string CustomAttributeNamedArgument = "System.Reflection.CustomAttributeNamedArgument"; + public const string BindingFlags = "System.Reflection.BindingFlags"; public static HashSet TypeClass { get; } = [ diff --git a/src/HotChocolate/Core/src/Types/Extensions/ResolverContextExtensions.cs b/src/HotChocolate/Core/src/Types/Extensions/ResolverContextExtensions.cs index fcee980f0bf..48c37d9d9f7 100644 --- a/src/HotChocolate/Core/src/Types/Extensions/ResolverContextExtensions.cs +++ b/src/HotChocolate/Core/src/Types/Extensions/ResolverContextExtensions.cs @@ -25,7 +25,7 @@ public static class ResolverContextExtensions /// could not be found or casted to . /// public static T? GetGlobalStateOrDefault( - this IResolverContext context, + this IPureResolverContext context, string name) { if (context is null) @@ -47,6 +47,43 @@ public static class ResolverContextExtensions return default; } + /// + /// Gets the global state for the specified , + /// or a default value if the state could not be resolved. + /// + /// The resolver context. + /// The name of the state. + /// The default value. + /// The type of the state. + /// + /// Returns the global state for the specified + /// or the default value of , if the state + /// could not be found or casted to . + /// + public static T GetGlobalStateOrDefault( + this IPureResolverContext context, + string name, + T defaultValue) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (string.IsNullOrEmpty(name)) + { + throw String_NullOrEmpty(nameof(name)); + } + + if (context.ContextData.TryGetValue(name, out var value) && + value is T casted) + { + return casted; + } + + return defaultValue; + } + /// /// Gets the global state for the specified , /// or throws if the state does not exist. @@ -58,7 +95,7 @@ public static class ResolverContextExtensions /// Returns the global state for the specified . /// public static T? GetGlobalState( - this IResolverContext context, + this IPureResolverContext context, string name) { if (context is null) @@ -101,7 +138,7 @@ public static class ResolverContextExtensions /// could not be found or casted to . /// public static T? GetScopedStateOrDefault( - this IResolverContext context, + this IPureResolverContext context, string name) { if (context is null) @@ -123,6 +160,43 @@ public static class ResolverContextExtensions return default; } + /// + /// Gets the scoped state for the specified , + /// or a default value if the state could not be resolved. + /// + /// The resolver context. + /// The name of the state. + /// The default value. + /// The type of the state. + /// + /// Returns the scoped state for the specified + /// or the default value of , if the state + /// could not be found or casted to . + /// + public static T GetScopedStateOrDefault( + this IPureResolverContext context, + string name, + T defaultValue) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (string.IsNullOrEmpty(name)) + { + throw String_NullOrEmpty(nameof(name)); + } + + if (context.ScopedContextData.TryGetValue(name, out var value) && + value is T casted) + { + return casted; + } + + return defaultValue; + } + /// /// Gets the scoped state for the specified , /// or throws if the state does not exist. @@ -134,7 +208,7 @@ public static class ResolverContextExtensions /// Returns the scoped state for the specified . /// public static T? GetScopedState( - this IResolverContext context, + this IPureResolverContext context, string name) { if (context is null) @@ -199,6 +273,43 @@ public static class ResolverContextExtensions return default; } + /// + /// Gets the local state for the specified , + /// or a default value if the state could not be resolved. + /// + /// The resolver context. + /// The name of the state. + /// The default value. + /// The type of the state. + /// + /// Returns the local state for the specified + /// or the default value of , if the state + /// could not be found or casted to . + /// + public static T GetLocalStateOrDefault( + this IResolverContext context, + string name, + T defaultValue) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (string.IsNullOrEmpty(name)) + { + throw String_NullOrEmpty(nameof(name)); + } + + if (context.LocalContextData.TryGetValue(name, out var value) && + value is T casted) + { + return casted; + } + + return defaultValue; + } + /// /// Gets the local state for the specified , /// or throws if the state does not exist. @@ -542,7 +653,7 @@ public static T GetEventMessage(this IResolverContext context) /// public static ClaimsPrincipal? GetUser(this IResolverContext context) => context.GetGlobalStateOrDefault(nameof(ClaimsPrincipal)); - + /// /// Checks if a field is selected in the current selection set. /// @@ -574,7 +685,7 @@ public static bool IsSelected(this IResolverContext context, string fieldName) ResolverContextExtensions_IsSelected_FieldNameEmpty, nameof(fieldName)); } - + if (!context.Selection.Type.IsCompositeType()) { return false; @@ -646,14 +757,14 @@ public static bool IsSelected(this IResolverContext context, string fieldName1, ResolverContextExtensions_IsSelected_FieldNameEmpty, nameof(fieldName1)); } - + if (string.IsNullOrWhiteSpace(fieldName2)) { throw new ArgumentException( ResolverContextExtensions_IsSelected_FieldNameEmpty, nameof(fieldName2)); } - + if (!context.Selection.Type.IsCompositeType()) { return false; @@ -737,21 +848,21 @@ public static bool IsSelected( ResolverContextExtensions_IsSelected_FieldNameEmpty, nameof(fieldName1)); } - + if (string.IsNullOrWhiteSpace(fieldName2)) { throw new ArgumentException( ResolverContextExtensions_IsSelected_FieldNameEmpty, nameof(fieldName2)); } - + if(string.IsNullOrWhiteSpace(fieldName3)) { throw new ArgumentException( ResolverContextExtensions_IsSelected_FieldNameEmpty, nameof(fieldName3)); } - + if (!context.Selection.Type.IsCompositeType()) { return false; @@ -797,7 +908,7 @@ public static bool IsSelected( return false; } - + /// /// Checks if a field is selected in the current selection set. /// @@ -813,19 +924,19 @@ public static bool IsSelected( /// is null. /// public static bool IsSelected( - this IResolverContext context, + this IResolverContext context, ISet fieldNames) { if(context is null) { throw new ArgumentNullException(nameof(context)); } - + if(fieldNames is null) { throw new ArgumentNullException(nameof(fieldNames)); } - + if (!context.Selection.Type.IsCompositeType()) { return false; diff --git a/src/HotChocolate/Core/src/Types/Internal/IParameterBinding.cs b/src/HotChocolate/Core/src/Types/Internal/IParameterBinding.cs new file mode 100644 index 00000000000..22b7e31f6d9 --- /dev/null +++ b/src/HotChocolate/Core/src/Types/Internal/IParameterBinding.cs @@ -0,0 +1,37 @@ +using System.Reflection; +using HotChocolate.Resolvers; + +namespace HotChocolate.Internal; + +public interface IParameterBinding +{ + T Execute(IResolverContext context); + + T Execute(IPureResolverContext context); +} + + +public abstract class ParameterBinding : IParameterBinding +{ + public virtual T Execute(IResolverContext context) + => Execute((IPureResolverContext)context); + + public abstract T Execute(IPureResolverContext context); +} + +public interface IParameterBindingFactory : IParameterHandler +{ + IParameterBinding Create(ParameterBindingContext context); +} + +public interface IParameterBindingResolver +{ + public IParameterBinding GetBinding(ParameterInfo parameter); +} + +public readonly ref struct ParameterBindingContext(ParameterInfo parameter, string argumentName) +{ + public ParameterInfo Parameter { get; } = parameter; + + public string ArgumentName { get; } = argumentName; +} diff --git a/src/HotChocolate/Core/src/Types/Resolvers/DefaultParameterBindingResolver.cs b/src/HotChocolate/Core/src/Types/Resolvers/DefaultParameterBindingResolver.cs new file mode 100644 index 00000000000..0464a99e3a2 --- /dev/null +++ b/src/HotChocolate/Core/src/Types/Resolvers/DefaultParameterBindingResolver.cs @@ -0,0 +1,89 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using HotChocolate.Internal; +using HotChocolate.Resolvers.Expressions.Parameters; + +namespace HotChocolate.Resolvers; + +public sealed class DefaultParameterBindingResolver : IParameterBindingResolver +{ + private readonly IParameterBindingFactory[] _bindings; + private readonly IParameterBindingFactory _defaultBinding; + + public DefaultParameterBindingResolver( + IServiceProvider applicationServices, + IEnumerable? customBindingFactories) + { + var serviceInspector = applicationServices?.GetService(); + + var bindingFactories = new List + { + new ParentParameterExpressionBuilder(), + new ServiceParameterExpressionBuilder(), + new ArgumentParameterExpressionBuilder(), + new GlobalStateParameterExpressionBuilder(), + new ScopedStateParameterExpressionBuilder(), + new LocalStateParameterExpressionBuilder(), + new IsSelectedParameterExpressionBuilder(), + new EventMessageParameterExpressionBuilder() + }; + + if (customBindingFactories is not null) + { + bindingFactories.AddRange( + customBindingFactories + .Where(t => !t.IsDefaultHandler) + .OfType()); + } + + if (serviceInspector is not null) + { + bindingFactories.Add(new InferredServiceParameterExpressionBuilder(serviceInspector)); + } + + + bindingFactories.Add(new DocumentParameterExpressionBuilder()); + bindingFactories.Add(new CancellationTokenParameterExpressionBuilder()); + bindingFactories.Add(new ResolverContextParameterExpressionBuilder()); + bindingFactories.Add(new PureResolverContextParameterExpressionBuilder()); + bindingFactories.Add(new SchemaParameterExpressionBuilder()); + bindingFactories.Add(new SelectionParameterExpressionBuilder()); + bindingFactories.Add(new FieldSyntaxParameterExpressionBuilder()); + bindingFactories.Add(new ObjectTypeParameterExpressionBuilder()); + bindingFactories.Add(new OperationDefinitionParameterExpressionBuilder()); + bindingFactories.Add(new OperationParameterExpressionBuilder()); + bindingFactories.Add(new FieldParameterExpressionBuilder()); + bindingFactories.Add(new ClaimsPrincipalParameterExpressionBuilder()); + bindingFactories.Add(new PathParameterExpressionBuilder()); + + if (customBindingFactories is not null) + { + bindingFactories.AddRange( + customBindingFactories + .Where(t => t.IsDefaultHandler) + .OfType()); + } + + _bindings = [.. bindingFactories]; + _defaultBinding = new ArgumentParameterExpressionBuilder(); + } + + public IParameterBinding GetBinding(ParameterInfo parameter) + { + var context = new ParameterBindingContext(parameter, parameter.Name); + + foreach (var binding in _bindings) + { + if (binding.CanHandle(parameter)) + { + return binding.Create(context); + } + } + + return _defaultBinding.Create(context); + } +} diff --git a/src/HotChocolate/Core/src/Types/Resolvers/DefaultResolverCompiler.cs b/src/HotChocolate/Core/src/Types/Resolvers/DefaultResolverCompiler.cs index b7ddea318f1..c0290fb4624 100644 --- a/src/HotChocolate/Core/src/Types/Resolvers/DefaultResolverCompiler.cs +++ b/src/HotChocolate/Core/src/Types/Resolvers/DefaultResolverCompiler.cs @@ -22,8 +22,7 @@ namespace HotChocolate.Resolvers; /// internal sealed class DefaultResolverCompiler : IResolverCompiler { - private static readonly IReadOnlyList _empty = - Array.Empty(); + private static readonly IReadOnlyList _empty = []; private static readonly ParameterExpression _context = Parameter(typeof(IResolverContext), "context"); diff --git a/src/HotChocolate/Core/src/Types/Resolvers/Expressions/Parameters/ArgumentParameterExpressionBuilder.cs b/src/HotChocolate/Core/src/Types/Resolvers/Expressions/Parameters/ArgumentParameterExpressionBuilder.cs index af1f843c893..b7bae7b9a47 100644 --- a/src/HotChocolate/Core/src/Types/Resolvers/Expressions/Parameters/ArgumentParameterExpressionBuilder.cs +++ b/src/HotChocolate/Core/src/Types/Resolvers/Expressions/Parameters/ArgumentParameterExpressionBuilder.cs @@ -10,7 +10,9 @@ namespace HotChocolate.Resolvers.Expressions.Parameters; -internal class ArgumentParameterExpressionBuilder : IParameterExpressionBuilder +internal class ArgumentParameterExpressionBuilder + : IParameterExpressionBuilder + , IParameterBindingFactory { private const string _argumentValue = nameof(IPureResolverContext.ArgumentValue); private const string _argumentLiteral = nameof(IPureResolverContext.ArgumentLiteral); @@ -83,4 +85,13 @@ public Expression Build(ParameterExpressionBuilderContext context) return Expression.Call(context.ResolverContext, argumentMethod, Expression.Constant(name)); } + + public IParameterBinding Create(ParameterBindingContext context) + => new ArgumentBinding(context.ArgumentName); + + private sealed class ArgumentBinding(string name) : ParameterBinding + { + public override T Execute(IPureResolverContext context) + => context.ArgumentValue(name); + } } diff --git a/src/HotChocolate/Core/src/Types/Resolvers/Expressions/Parameters/CancellationTokenParameterExpressionBuilder.cs b/src/HotChocolate/Core/src/Types/Resolvers/Expressions/Parameters/CancellationTokenParameterExpressionBuilder.cs index 28504d22c30..989929bb285 100644 --- a/src/HotChocolate/Core/src/Types/Resolvers/Expressions/Parameters/CancellationTokenParameterExpressionBuilder.cs +++ b/src/HotChocolate/Core/src/Types/Resolvers/Expressions/Parameters/CancellationTokenParameterExpressionBuilder.cs @@ -1,3 +1,4 @@ +using System; using System.Diagnostics; using System.Linq.Expressions; using System.Reflection; @@ -9,7 +10,10 @@ namespace HotChocolate.Resolvers.Expressions.Parameters; -internal sealed class CancellationTokenParameterExpressionBuilder : IParameterExpressionBuilder +internal sealed class CancellationTokenParameterExpressionBuilder + : IParameterExpressionBuilder + , IParameterBindingFactory + , IParameterBinding { private static readonly PropertyInfo _cancellationToken = ContextType.GetProperty(nameof(IResolverContext.RequestAborted))!; @@ -30,4 +34,13 @@ public bool CanHandle(ParameterInfo parameter) public Expression Build(ParameterExpressionBuilderContext context) => Expression.Property(context.ResolverContext, _cancellationToken); + + public IParameterBinding Create(ParameterBindingContext context) + => this; + + public T Execute(IResolverContext context) + => (T)(object)context.RequestAborted; + + public T Execute(IPureResolverContext context) + => throw new NotSupportedException(); } diff --git a/src/HotChocolate/Core/src/Types/Resolvers/Expressions/Parameters/ClaimsPrincipalParameterExpressionBuilder.cs b/src/HotChocolate/Core/src/Types/Resolvers/Expressions/Parameters/ClaimsPrincipalParameterExpressionBuilder.cs index 8d80ebdb328..fa1107f9e27 100644 --- a/src/HotChocolate/Core/src/Types/Resolvers/Expressions/Parameters/ClaimsPrincipalParameterExpressionBuilder.cs +++ b/src/HotChocolate/Core/src/Types/Resolvers/Expressions/Parameters/ClaimsPrincipalParameterExpressionBuilder.cs @@ -11,7 +11,10 @@ namespace HotChocolate.Resolvers.Expressions.Parameters; -internal sealed class ClaimsPrincipalParameterExpressionBuilder : IParameterExpressionBuilder +internal sealed class ClaimsPrincipalParameterExpressionBuilder + : IParameterExpressionBuilder + , IParameterBindingFactory + , IParameterBinding { public ArgumentKind Kind => ArgumentKind.Custom; @@ -52,4 +55,13 @@ public Expression Build(ParameterExpressionBuilderContext context) TypeResources.ClaimsPrincipalParameterExpressionBuilder_NoClaimsFound, nameof(context)); } + + public IParameterBinding Create(ParameterBindingContext context) + => this; + + public T Execute(IResolverContext context) + => context.GetGlobalState(nameof(ClaimsPrincipal))!; + + public T Execute(IPureResolverContext context) + => context.GetGlobalState(nameof(ClaimsPrincipal))!; } diff --git a/src/HotChocolate/Core/src/Types/Resolvers/Expressions/Parameters/DocumentParameterExpressionBuilder.cs b/src/HotChocolate/Core/src/Types/Resolvers/Expressions/Parameters/DocumentParameterExpressionBuilder.cs index 9d17123f7bb..a7134ee3756 100644 --- a/src/HotChocolate/Core/src/Types/Resolvers/Expressions/Parameters/DocumentParameterExpressionBuilder.cs +++ b/src/HotChocolate/Core/src/Types/Resolvers/Expressions/Parameters/DocumentParameterExpressionBuilder.cs @@ -1,13 +1,15 @@ +#nullable enable + using System.Reflection; using HotChocolate.Internal; using HotChocolate.Language; -#nullable enable - namespace HotChocolate.Resolvers.Expressions.Parameters; internal sealed class DocumentParameterExpressionBuilder : LambdaParameterExpressionBuilder + , IParameterBindingFactory + , IParameterBinding { public DocumentParameterExpressionBuilder() : base(ctx => ctx.Operation.Document) @@ -18,4 +20,13 @@ public DocumentParameterExpressionBuilder() public override bool CanHandle(ParameterInfo parameter) => typeof(DocumentNode) == parameter.ParameterType; + + public IParameterBinding Create(ParameterBindingContext context) + => this; + + public T Execute(IResolverContext context) + => (T)(object)context.Operation.Document; + + public T Execute(IPureResolverContext context) + => (T)(object)context.Operation.Document; } diff --git a/src/HotChocolate/Core/src/Types/Resolvers/Expressions/Parameters/EventMessageParameterExpressionBuilder.cs b/src/HotChocolate/Core/src/Types/Resolvers/Expressions/Parameters/EventMessageParameterExpressionBuilder.cs index 32d1eab9c7b..dc4103cd432 100644 --- a/src/HotChocolate/Core/src/Types/Resolvers/Expressions/Parameters/EventMessageParameterExpressionBuilder.cs +++ b/src/HotChocolate/Core/src/Types/Resolvers/Expressions/Parameters/EventMessageParameterExpressionBuilder.cs @@ -11,6 +11,8 @@ namespace HotChocolate.Resolvers.Expressions.Parameters; internal sealed class EventMessageParameterExpressionBuilder : LambdaParameterExpressionBuilder + , IParameterBindingFactory + , IParameterBinding { public EventMessageParameterExpressionBuilder() : base(ctx => GetEventMessage(ctx.ScopedContextData)) @@ -25,14 +27,26 @@ public override bool CanHandle(ParameterInfo parameter) public override Expression Build(ParameterExpressionBuilderContext context) => Expression.Convert(base.Build(context), context.Parameter.ParameterType); + public IParameterBinding Create(ParameterBindingContext context) + => this; + + public T Execute(IResolverContext context) + => GetEventMessage(context); + + public T Execute(IPureResolverContext context) + => GetEventMessage(context); + private static object GetEventMessage(IImmutableDictionary contextData) { if (!contextData.TryGetValue(WellKnownContextData.EventMessage, out var message) || message is null) { - throw new InvalidOperationException( - EventMessageParameterExpressionBuilder_MessageNotFound); + throw new InvalidOperationException(EventMessageParameterExpressionBuilder_MessageNotFound); } return message; } + + private static T GetEventMessage(IPureResolverContext context) + => context.GetScopedStateOrDefault(WellKnownContextData.EventMessage) ?? + throw new InvalidOperationException(EventMessageParameterExpressionBuilder_MessageNotFound); } diff --git a/src/HotChocolate/Core/src/Types/Resolvers/Expressions/Parameters/FieldParameterExpressionBuilder.cs b/src/HotChocolate/Core/src/Types/Resolvers/Expressions/Parameters/FieldParameterExpressionBuilder.cs index fcf2e861361..e35a06e6140 100644 --- a/src/HotChocolate/Core/src/Types/Resolvers/Expressions/Parameters/FieldParameterExpressionBuilder.cs +++ b/src/HotChocolate/Core/src/Types/Resolvers/Expressions/Parameters/FieldParameterExpressionBuilder.cs @@ -9,6 +9,8 @@ namespace HotChocolate.Resolvers.Expressions.Parameters; internal sealed class FieldParameterExpressionBuilder : LambdaParameterExpressionBuilder + , IParameterBindingFactory + , IParameterBinding { public FieldParameterExpressionBuilder() : base(ctx => ctx.Selection.Field) @@ -29,4 +31,13 @@ public override Expression Build(ParameterExpressionBuilderContext context) ? Expression.Convert(expression, parameter.ParameterType) : expression; } + + public IParameterBinding Create(ParameterBindingContext context) + => this; + + public T Execute(IResolverContext context) + => (T)(object)context.Selection.Field; + + public T Execute(IPureResolverContext context) + => (T)(object)context.Selection.Field; } diff --git a/src/HotChocolate/Core/src/Types/Resolvers/Expressions/Parameters/FieldSyntaxParameterExpressionBuilder.cs b/src/HotChocolate/Core/src/Types/Resolvers/Expressions/Parameters/FieldSyntaxParameterExpressionBuilder.cs index 04b18bc6961..4430be5c361 100644 --- a/src/HotChocolate/Core/src/Types/Resolvers/Expressions/Parameters/FieldSyntaxParameterExpressionBuilder.cs +++ b/src/HotChocolate/Core/src/Types/Resolvers/Expressions/Parameters/FieldSyntaxParameterExpressionBuilder.cs @@ -8,6 +8,8 @@ namespace HotChocolate.Resolvers.Expressions.Parameters; internal sealed class FieldSyntaxParameterExpressionBuilder : LambdaParameterExpressionBuilder + , IParameterBindingFactory + , IParameterBinding { public FieldSyntaxParameterExpressionBuilder() : base(ctx => ctx.Selection.SyntaxNode) @@ -18,4 +20,13 @@ public FieldSyntaxParameterExpressionBuilder() public override bool CanHandle(ParameterInfo parameter) => typeof(FieldNode) == parameter.ParameterType; + + public IParameterBinding Create(ParameterBindingContext context) + => this; + + public T Execute(IResolverContext context) + => (T)(object)context.Selection.Field; + + public T Execute(IPureResolverContext context) + => (T)(object)context.Selection.Field; } diff --git a/src/HotChocolate/Core/src/Types/Resolvers/Expressions/Parameters/GlobalStateParameterExpressionBuilder.cs b/src/HotChocolate/Core/src/Types/Resolvers/Expressions/Parameters/GlobalStateParameterExpressionBuilder.cs index 0696c4f2ee1..0d05106dde3 100644 --- a/src/HotChocolate/Core/src/Types/Resolvers/Expressions/Parameters/GlobalStateParameterExpressionBuilder.cs +++ b/src/HotChocolate/Core/src/Types/Resolvers/Expressions/Parameters/GlobalStateParameterExpressionBuilder.cs @@ -1,3 +1,4 @@ +using System; using System.Linq; using System.Linq.Expressions; using System.Reflection; @@ -9,7 +10,10 @@ namespace HotChocolate.Resolvers.Expressions.Parameters; -internal sealed class GlobalStateParameterExpressionBuilder : IParameterExpressionBuilder +internal sealed class GlobalStateParameterExpressionBuilder + : IParameterExpressionBuilder + , IParameterBindingFactory + , IParameterBinding { private static readonly PropertyInfo _contextData = typeof(IHasContextData).GetProperty( @@ -96,4 +100,13 @@ private Expression BuildGetter( .GetFlags(parameter).FirstOrDefault() ?? false, typeof(bool))); } + + public IParameterBinding Create(ParameterBindingContext context) + => this; + + public T Execute(IResolverContext context) + => throw new NotSupportedException(); + + public T Execute(IPureResolverContext context) + => throw new NotSupportedException(); } diff --git a/src/HotChocolate/Core/src/Types/Resolvers/Expressions/Parameters/ImplicitArgumentParameterExpressionBuilder.cs b/src/HotChocolate/Core/src/Types/Resolvers/Expressions/Parameters/ImplicitArgumentParameterExpressionBuilder.cs index 48098b8de91..abddd8a66aa 100644 --- a/src/HotChocolate/Core/src/Types/Resolvers/Expressions/Parameters/ImplicitArgumentParameterExpressionBuilder.cs +++ b/src/HotChocolate/Core/src/Types/Resolvers/Expressions/Parameters/ImplicitArgumentParameterExpressionBuilder.cs @@ -4,7 +4,8 @@ namespace HotChocolate.Resolvers.Expressions.Parameters; -internal class ImplicitArgumentParameterExpressionBuilder : ArgumentParameterExpressionBuilder +internal class ImplicitArgumentParameterExpressionBuilder + : ArgumentParameterExpressionBuilder { public override bool CanHandle(ParameterInfo parameter) => true; diff --git a/src/HotChocolate/Core/src/Types/Resolvers/Expressions/Parameters/InferredServiceParameterExpressionBuilder.cs b/src/HotChocolate/Core/src/Types/Resolvers/Expressions/Parameters/InferredServiceParameterExpressionBuilder.cs index 9b75acf7418..9474a93caa4 100644 --- a/src/HotChocolate/Core/src/Types/Resolvers/Expressions/Parameters/InferredServiceParameterExpressionBuilder.cs +++ b/src/HotChocolate/Core/src/Types/Resolvers/Expressions/Parameters/InferredServiceParameterExpressionBuilder.cs @@ -13,6 +13,8 @@ namespace HotChocolate.Resolvers.Expressions.Parameters; /// internal sealed class InferredServiceParameterExpressionBuilder(IServiceProviderIsService serviceInspector) : IParameterExpressionBuilder + , IParameterBindingFactory + , IParameterBinding { public ArgumentKind Kind => ArgumentKind.Service; @@ -28,10 +30,19 @@ public bool CanHandle(ParameterInfo parameter) { return serviceInspector.IsService(parameter.ParameterType.GetGenericArguments()[0]); } - + return serviceInspector.IsService(parameter.ParameterType); } public Expression Build(ParameterExpressionBuilderContext context) => ServiceExpressionHelper.Build(context.Parameter, context.ResolverContext); -} \ No newline at end of file + + public IParameterBinding Create(ParameterBindingContext context) + => this; + + public T Execute(IResolverContext context) where T : notnull + => context.Service(); + + public T Execute(IPureResolverContext context) where T : notnull + => context.Service(); +} diff --git a/src/HotChocolate/Core/src/Types/Resolvers/Expressions/Parameters/IsSelectedParameterExpressionBuilder.cs b/src/HotChocolate/Core/src/Types/Resolvers/Expressions/Parameters/IsSelectedParameterExpressionBuilder.cs index 65df6560e6a..50ea1a48cee 100644 --- a/src/HotChocolate/Core/src/Types/Resolvers/Expressions/Parameters/IsSelectedParameterExpressionBuilder.cs +++ b/src/HotChocolate/Core/src/Types/Resolvers/Expressions/Parameters/IsSelectedParameterExpressionBuilder.cs @@ -14,7 +14,10 @@ namespace HotChocolate.Resolvers.Expressions.Parameters; -internal sealed class IsSelectedParameterExpressionBuilder : IParameterExpressionBuilder, IParameterFieldConfiguration +internal sealed class IsSelectedParameterExpressionBuilder + : IParameterExpressionBuilder + , IParameterFieldConfiguration + , IParameterBindingFactory { public ArgumentKind Kind => ArgumentKind.LocalState; @@ -33,6 +36,9 @@ public Expression Build(ParameterExpressionBuilderContext context) return Expression.Invoke(expr, context.ResolverContext); } + public IParameterBinding Create(ParameterBindingContext context) + => new IsSelectedBinding($"isSelected.{context.Parameter.Name}"); + public void ApplyConfiguration(ParameterInfo parameter, ObjectFieldDescriptor descriptor) { var attribute = parameter.GetCustomAttribute()!; @@ -200,4 +206,13 @@ public IsSelectedContext(ISchema schema, ISelectionCollection selections) public bool AllSelected { get; set; } = true; } + + private class IsSelectedBinding(string key) : IParameterBinding + { + public T Execute(IResolverContext context) + => context.GetLocalState(key)!; + + public T Execute(IPureResolverContext context) + => throw new NotSupportedException(); + } } diff --git a/src/HotChocolate/Core/src/Types/Resolvers/Expressions/Parameters/ObjectTypeParameterExpressionBuilder.cs b/src/HotChocolate/Core/src/Types/Resolvers/Expressions/Parameters/ObjectTypeParameterExpressionBuilder.cs index 440a0e2c271..a995f4e169d 100644 --- a/src/HotChocolate/Core/src/Types/Resolvers/Expressions/Parameters/ObjectTypeParameterExpressionBuilder.cs +++ b/src/HotChocolate/Core/src/Types/Resolvers/Expressions/Parameters/ObjectTypeParameterExpressionBuilder.cs @@ -9,6 +9,8 @@ namespace HotChocolate.Resolvers.Expressions.Parameters; internal sealed class ObjectTypeParameterExpressionBuilder : LambdaParameterExpressionBuilder + , IParameterBindingFactory + , IParameterBinding { public ObjectTypeParameterExpressionBuilder() : base(ctx => ctx.ObjectType) @@ -29,4 +31,13 @@ public override Expression Build(ParameterExpressionBuilderContext context) ? Expression.Convert(expression, typeof(ObjectType)) : expression; } + + public IParameterBinding Create(ParameterBindingContext context) + => this; + + public T Execute(IResolverContext context) + => (T)(object)context.ObjectType; + + public T Execute(IPureResolverContext context) + => (T)(object)context.ObjectType; } diff --git a/src/HotChocolate/Core/src/Types/Resolvers/Expressions/Parameters/OperationDefinitionParameterExpressionBuilder.cs b/src/HotChocolate/Core/src/Types/Resolvers/Expressions/Parameters/OperationDefinitionParameterExpressionBuilder.cs index cfcf0c0b349..816a3f16d2a 100644 --- a/src/HotChocolate/Core/src/Types/Resolvers/Expressions/Parameters/OperationDefinitionParameterExpressionBuilder.cs +++ b/src/HotChocolate/Core/src/Types/Resolvers/Expressions/Parameters/OperationDefinitionParameterExpressionBuilder.cs @@ -8,6 +8,8 @@ namespace HotChocolate.Resolvers.Expressions.Parameters; internal sealed class OperationDefinitionParameterExpressionBuilder : LambdaParameterExpressionBuilder + , IParameterBindingFactory + , IParameterBinding { public OperationDefinitionParameterExpressionBuilder() : base(ctx => ctx.Operation.Definition) @@ -18,4 +20,13 @@ public OperationDefinitionParameterExpressionBuilder() public override bool CanHandle(ParameterInfo parameter) => typeof(OperationDefinitionNode) == parameter.ParameterType; + + public IParameterBinding Create(ParameterBindingContext context) + => this; + + public T Execute(IResolverContext context) + => (T)(object)context.Operation.Definition; + + public T Execute(IPureResolverContext context) + => (T)(object)context.Operation.Definition; } diff --git a/src/HotChocolate/Core/src/Types/Resolvers/Expressions/Parameters/OperationParameterExpressionBuilder.cs b/src/HotChocolate/Core/src/Types/Resolvers/Expressions/Parameters/OperationParameterExpressionBuilder.cs index e5217aa3460..1eb137b2905 100644 --- a/src/HotChocolate/Core/src/Types/Resolvers/Expressions/Parameters/OperationParameterExpressionBuilder.cs +++ b/src/HotChocolate/Core/src/Types/Resolvers/Expressions/Parameters/OperationParameterExpressionBuilder.cs @@ -7,6 +7,8 @@ namespace HotChocolate.Resolvers.Expressions.Parameters; internal sealed class OperationParameterExpressionBuilder : LambdaParameterExpressionBuilder + , IParameterBindingFactory + , IParameterBinding { public OperationParameterExpressionBuilder() : base(ctx => ctx.Operation) @@ -17,4 +19,13 @@ public OperationParameterExpressionBuilder() public override bool CanHandle(ParameterInfo parameter) => typeof(IOperation) == parameter.ParameterType; + + public IParameterBinding Create(ParameterBindingContext context) + => this; + + public T Execute(IResolverContext context) + => (T)(object)context.Operation; + + public T Execute(IPureResolverContext context) + => (T)(object)context.Operation; } diff --git a/src/HotChocolate/Core/src/Types/Resolvers/Expressions/Parameters/ParentParameterExpressionBuilder.cs b/src/HotChocolate/Core/src/Types/Resolvers/Expressions/Parameters/ParentParameterExpressionBuilder.cs index 83a48d3cd66..f706e39f577 100644 --- a/src/HotChocolate/Core/src/Types/Resolvers/Expressions/Parameters/ParentParameterExpressionBuilder.cs +++ b/src/HotChocolate/Core/src/Types/Resolvers/Expressions/Parameters/ParentParameterExpressionBuilder.cs @@ -14,7 +14,10 @@ namespace HotChocolate.Resolvers.Expressions.Parameters; /// Parameters representing the parent object must be annotated with /// . /// -internal sealed class ParentParameterExpressionBuilder : IParameterExpressionBuilder +internal sealed class ParentParameterExpressionBuilder + : IParameterExpressionBuilder + , IParameterBindingFactory + , IParameterBinding { private const string _parent = nameof(IPureResolverContext.Parent); private static readonly MethodInfo _getParentMethod = @@ -39,4 +42,13 @@ public Expression Build(ParameterExpressionBuilderContext context) var argumentMethod = _getParentMethod.MakeGenericMethod(parameterType); return Expression.Call(context.ResolverContext, argumentMethod); } + + public IParameterBinding Create(ParameterBindingContext context) + => this; + + public T Execute(IResolverContext context) + => context.Parent(); + + public T Execute(IPureResolverContext context) + => context.Parent(); } diff --git a/src/HotChocolate/Core/src/Types/Resolvers/Expressions/Parameters/PathParameterExpressionBuilder.cs b/src/HotChocolate/Core/src/Types/Resolvers/Expressions/Parameters/PathParameterExpressionBuilder.cs index 8c50d00a271..7cff21dbb25 100644 --- a/src/HotChocolate/Core/src/Types/Resolvers/Expressions/Parameters/PathParameterExpressionBuilder.cs +++ b/src/HotChocolate/Core/src/Types/Resolvers/Expressions/Parameters/PathParameterExpressionBuilder.cs @@ -7,6 +7,8 @@ namespace HotChocolate.Resolvers.Expressions.Parameters; internal sealed class PathParameterExpressionBuilder : LambdaParameterExpressionBuilder + , IParameterBindingFactory + , IParameterBinding { public PathParameterExpressionBuilder() : base(ctx => ctx.Path) @@ -17,4 +19,13 @@ public PathParameterExpressionBuilder() public override bool CanHandle(ParameterInfo parameter) => parameter.ParameterType == typeof(Path); + + public IParameterBinding Create(ParameterBindingContext context) + => this; + + public T Execute(IResolverContext context) + => (T)(object)context.Path; + + public T Execute(IPureResolverContext context) + => (T)(object)context.Path; } diff --git a/src/HotChocolate/Core/src/Types/Resolvers/Expressions/Parameters/PureResolverContextParameterExpressionBuilder.cs b/src/HotChocolate/Core/src/Types/Resolvers/Expressions/Parameters/PureResolverContextParameterExpressionBuilder.cs index a77fe1694ad..cdaeb845596 100644 --- a/src/HotChocolate/Core/src/Types/Resolvers/Expressions/Parameters/PureResolverContextParameterExpressionBuilder.cs +++ b/src/HotChocolate/Core/src/Types/Resolvers/Expressions/Parameters/PureResolverContextParameterExpressionBuilder.cs @@ -1,3 +1,4 @@ +using System; using System.Linq.Expressions; using System.Reflection; using HotChocolate.Internal; @@ -8,6 +9,8 @@ namespace HotChocolate.Resolvers.Expressions.Parameters; internal sealed class PureResolverContextParameterExpressionBuilder : IParameterExpressionBuilder + , IParameterBindingFactory + , IParameterBinding { public ArgumentKind Kind => ArgumentKind.Context; @@ -20,4 +23,13 @@ public bool CanHandle(ParameterInfo parameter) public Expression Build(ParameterExpressionBuilderContext context) => context.ResolverContext; + + public IParameterBinding Create(ParameterBindingContext context) + => this; + + public T Execute(IResolverContext context) + => throw new NotSupportedException(); + + public T Execute(IPureResolverContext context) + => (T)(object)context; } diff --git a/src/HotChocolate/Core/src/Types/Resolvers/Expressions/Parameters/ResolverContextParameterExpressionBuilder.cs b/src/HotChocolate/Core/src/Types/Resolvers/Expressions/Parameters/ResolverContextParameterExpressionBuilder.cs index 212757d2714..1fddcfbb1a2 100644 --- a/src/HotChocolate/Core/src/Types/Resolvers/Expressions/Parameters/ResolverContextParameterExpressionBuilder.cs +++ b/src/HotChocolate/Core/src/Types/Resolvers/Expressions/Parameters/ResolverContextParameterExpressionBuilder.cs @@ -1,3 +1,4 @@ +using System; using System.Linq.Expressions; using System.Reflection; using HotChocolate.Internal; @@ -6,7 +7,10 @@ namespace HotChocolate.Resolvers.Expressions.Parameters; -internal sealed class ResolverContextParameterExpressionBuilder : IParameterExpressionBuilder +internal sealed class ResolverContextParameterExpressionBuilder + : IParameterExpressionBuilder + , IParameterBindingFactory + , IParameterBinding { public ArgumentKind Kind => ArgumentKind.Context; @@ -19,4 +23,13 @@ public bool CanHandle(ParameterInfo parameter) public Expression Build(ParameterExpressionBuilderContext context) => context.ResolverContext; + + public IParameterBinding Create(ParameterBindingContext context) + => this; + + public T Execute(IResolverContext context) + => (T)(object)context; + + public T Execute(IPureResolverContext context) + => throw new NotSupportedException(); } diff --git a/src/HotChocolate/Core/src/Types/Resolvers/Expressions/Parameters/SchemaParameterExpressionBuilder.cs b/src/HotChocolate/Core/src/Types/Resolvers/Expressions/Parameters/SchemaParameterExpressionBuilder.cs index 6846c102c24..901a3e44ba9 100644 --- a/src/HotChocolate/Core/src/Types/Resolvers/Expressions/Parameters/SchemaParameterExpressionBuilder.cs +++ b/src/HotChocolate/Core/src/Types/Resolvers/Expressions/Parameters/SchemaParameterExpressionBuilder.cs @@ -7,7 +7,10 @@ namespace HotChocolate.Resolvers.Expressions.Parameters; -internal sealed class SchemaParameterExpressionBuilder : IParameterExpressionBuilder +internal sealed class SchemaParameterExpressionBuilder + : IParameterExpressionBuilder + , IParameterBindingFactory + , IParameterBinding { private static readonly PropertyInfo _schema = PureContextType.GetProperty(nameof(IPureResolverContext.Schema))!; @@ -26,4 +29,13 @@ public Expression Build(ParameterExpressionBuilderContext context) => Expression.Convert( Expression.Property(context.ResolverContext, _schema), context.Parameter.ParameterType); + + public IParameterBinding Create(ParameterBindingContext context) + => this; + + public T Execute(IResolverContext context) + => (T)(object)context.Schema; + + public T Execute(IPureResolverContext context) + => (T)(object)context.Schema; } diff --git a/src/HotChocolate/Core/src/Types/Resolvers/Expressions/Parameters/ScopedStateParameterExpressionBuilder.cs b/src/HotChocolate/Core/src/Types/Resolvers/Expressions/Parameters/ScopedStateParameterExpressionBuilder.cs index 1076db98ef0..760cfabf25e 100644 --- a/src/HotChocolate/Core/src/Types/Resolvers/Expressions/Parameters/ScopedStateParameterExpressionBuilder.cs +++ b/src/HotChocolate/Core/src/Types/Resolvers/Expressions/Parameters/ScopedStateParameterExpressionBuilder.cs @@ -9,7 +9,10 @@ namespace HotChocolate.Resolvers.Expressions.Parameters; -internal class ScopedStateParameterExpressionBuilder : IParameterExpressionBuilder +internal class ScopedStateParameterExpressionBuilder + : IParameterExpressionBuilder + , IParameterBindingFactory + , IParameterBinding { private static readonly MethodInfo _getScopedState = typeof(ExpressionHelper).GetMethod( @@ -123,4 +126,13 @@ protected virtual bool ResolveDefaultIfNotExistsParameterValue( } return false; } + + public IParameterBinding Create(ParameterBindingContext context) + => this; + + public T Execute(IResolverContext context) + => throw new NotSupportedException(); + + public T Execute(IPureResolverContext context) + => throw new NotSupportedException(); } diff --git a/src/HotChocolate/Core/src/Types/Resolvers/Expressions/Parameters/SelectionParameterExpressionBuilder.cs b/src/HotChocolate/Core/src/Types/Resolvers/Expressions/Parameters/SelectionParameterExpressionBuilder.cs index 115cf027a4c..e614093a094 100644 --- a/src/HotChocolate/Core/src/Types/Resolvers/Expressions/Parameters/SelectionParameterExpressionBuilder.cs +++ b/src/HotChocolate/Core/src/Types/Resolvers/Expressions/Parameters/SelectionParameterExpressionBuilder.cs @@ -9,6 +9,8 @@ namespace HotChocolate.Resolvers.Expressions.Parameters; internal sealed class SelectionParameterExpressionBuilder() : LambdaParameterExpressionBuilder(ctx => ctx.Selection) + , IParameterBindingFactory + , IParameterBinding { public override ArgumentKind Kind => ArgumentKind.Selection; @@ -17,4 +19,13 @@ public override bool CanHandle(ParameterInfo parameter) public override Expression Build(ParameterExpressionBuilderContext context) => Expression.Convert(base.Build(context), context.Parameter.ParameterType); + + public IParameterBinding Create(ParameterBindingContext context) + => this; + + public T Execute(IResolverContext context) + => (T)(object)context.Selection; + + public T Execute(IPureResolverContext context) + => (T)(object)context.Selection; } diff --git a/src/HotChocolate/Core/src/Types/Resolvers/Expressions/Parameters/ServiceParameterExpressionBuilder.cs b/src/HotChocolate/Core/src/Types/Resolvers/Expressions/Parameters/ServiceParameterExpressionBuilder.cs index e8d16b4397b..5273601ea35 100644 --- a/src/HotChocolate/Core/src/Types/Resolvers/Expressions/Parameters/ServiceParameterExpressionBuilder.cs +++ b/src/HotChocolate/Core/src/Types/Resolvers/Expressions/Parameters/ServiceParameterExpressionBuilder.cs @@ -13,6 +13,7 @@ namespace HotChocolate.Resolvers.Expressions.Parameters; /// internal sealed class ServiceParameterExpressionBuilder : IParameterExpressionBuilder + , IParameterBindingFactory { public ArgumentKind Kind => ArgumentKind.Service; @@ -35,4 +36,36 @@ public Expression Build(ParameterExpressionBuilderContext context) #endif return ServiceExpressionHelper.Build(context.Parameter, context.ResolverContext); } + + public IParameterBinding Create(ParameterBindingContext context) +#if NET8_0_OR_GREATER + => new ServiceParameterBinding(context.Parameter); +#else + => new ServiceParameterBinding(); +#endif + + private sealed class ServiceParameterBinding : ParameterBinding + { +#if NET8_0_OR_GREATER + public ServiceParameterBinding(ParameterInfo parameter) + { + var attribute = parameter.GetCustomAttribute(); + Key = attribute?.Key; + } + + public string? Key { get; } +#endif + + public override T Execute(IPureResolverContext context) + { +#if NET8_0_OR_GREATER + if (Key is not null) + { + return context.Service(Key)!; + } + +#endif + return context.Service(); + } + } } diff --git a/src/HotChocolate/Core/src/Types/Types/Descriptors/Conventions/DefaultTypeInspector.cs b/src/HotChocolate/Core/src/Types/Types/Descriptors/Conventions/DefaultTypeInspector.cs index 452e1614f0f..7552159d36b 100644 --- a/src/HotChocolate/Core/src/Types/Types/Descriptors/Conventions/DefaultTypeInspector.cs +++ b/src/HotChocolate/Core/src/Types/Types/Descriptors/Conventions/DefaultTypeInspector.cs @@ -915,4 +915,4 @@ private bool TryGetDefaultValueFromConstructor( return false; } -} \ No newline at end of file +} diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/AddressDataLoader.cs b/src/HotChocolate/Core/test/Types.Analyzers.Tests/AddressDataLoader.cs deleted file mode 100644 index a0cf632734d..00000000000 --- a/src/HotChocolate/Core/test/Types.Analyzers.Tests/AddressDataLoader.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using GreenDonut; - -namespace HotChocolate.Types; - -public class AddressDataLoader(IBatchScheduler batchScheduler, DataLoaderOptions options) - : BatchDataLoader(batchScheduler, options) -{ - protected override Task> LoadBatchAsync( - IReadOnlyList keys, - CancellationToken cancellationToken) - { - return Task.FromResult>(keys.ToDictionary(t => t)); - } -} diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/AnnotationBasedSchemaTests.cs b/src/HotChocolate/Core/test/Types.Analyzers.Tests/AnnotationBasedSchemaTests.cs deleted file mode 100644 index 38ffb0f5e71..00000000000 --- a/src/HotChocolate/Core/test/Types.Analyzers.Tests/AnnotationBasedSchemaTests.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System.Threading.Tasks; -using HotChocolate.Execution; -using Microsoft.Extensions.DependencyInjection; -using CookieCrumble; - -namespace HotChocolate.Types; - -public class SchemaTests -{ - [Fact] - public async Task SchemaSnapshot() - { - var schema = - await new ServiceCollection() - .AddGraphQL() - .AddGlobalObjectIdentification() - .AddCustomModule() - .BuildSchemaAsync(); - - schema.MatchMarkdownSnapshot(); - } - - [Fact] - public async Task ExecuteRootField() - { - var services = new ServiceCollection() - .AddGraphQL() - .AddGlobalObjectIdentification() - .AddCustomModule(); - - var result = await services.ExecuteRequestAsync("{ foo }"); - - result.MatchMarkdownSnapshot(); - } - - [Fact] - public async Task ExecuteWithMiddleware() - { - var services = new ServiceCollection() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddGraphQL() - .AddCustomModule() - .UseRequest() - .UseDefaultPipeline(); - - var result = await services.ExecuteRequestAsync("{ foo }"); - - result.MatchMarkdownSnapshot(); - } -} diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/BookType.cs b/src/HotChocolate/Core/test/Types.Analyzers.Tests/BookType.cs deleted file mode 100644 index bdef52b8947..00000000000 --- a/src/HotChocolate/Core/test/Types.Analyzers.Tests/BookType.cs +++ /dev/null @@ -1,37 +0,0 @@ -namespace HotChocolate.Types; - -public class BookType : ObjectType -{ - protected override void Configure(IObjectTypeDescriptor descriptor) - { - descriptor.Name("SomeBook"); - } -} - -public class Book -{ - public string? Title { get; set; } - - public Author? Author => null; - - public Publisher? Publisher => null; -} - -public class Author -{ - public string Name { get; set; } = default!; -} - -public readonly record struct Publisher(string Name); - -[ObjectType] -public static partial class AuthorNode -{ - public static string Address([Parent] Author author) => "something"; -} - -[ObjectType] -public static partial class PublisherNode -{ - public static string Company([Parent] Publisher author) => "something"; -} diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/CustomEnum.cs b/src/HotChocolate/Core/test/Types.Analyzers.Tests/CustomEnum.cs deleted file mode 100644 index 54c65dac09e..00000000000 --- a/src/HotChocolate/Core/test/Types.Analyzers.Tests/CustomEnum.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace HotChocolate.Types; - -[EnumType("CustomEnum")] -public enum MyEnum -{ - Abc, - Def, -} diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/IntegrationTests.cs b/src/HotChocolate/Core/test/Types.Analyzers.Tests/IntegrationTests.cs new file mode 100644 index 00000000000..c0e51efe36c --- /dev/null +++ b/src/HotChocolate/Core/test/Types.Analyzers.Tests/IntegrationTests.cs @@ -0,0 +1,116 @@ +using System; +using System.Threading.Tasks; +using CookieCrumble; +using HotChocolate.Execution; +using Microsoft.Extensions.DependencyInjection; + +namespace HotChocolate.Types; + +public class IntegrationTests +{ + [Fact] + public async Task Schema() + { + // arrange + var services = CreateApplicationServices(); + + // act + var executor = await services.GetRequiredService().GetRequestExecutorAsync(); + + // assert + executor.Schema.MatchSnapshot(); + } + + [Fact] + public async Task Query_Books_And_Authors() + { + // arrange + var services = CreateApplicationServices(); + var executor = await services.GetRequiredService().GetRequestExecutorAsync(); + + // act + var result = await executor.ExecuteAsync( + """ + { + books { + nodes { + id + title + genre + author { + id + name + } + } + } + } + """); + + // assert + result.MatchMarkdownSnapshot(); + } + + [Fact] + public async Task Class_Request_Middleware() + { + // arrange + var services = CreateApplicationServices( + services => + { + services + .AddSingleton() + .AddSingleton() + .AddScoped(); + + services + .AddGraphQLServer() + .UseRequest() + .UseDefaultPipeline(); + }); + + var executor = await services.GetRequiredService().GetRequestExecutorAsync(); + + // act + var result = await executor.ExecuteAsync( + """ + { + books { + nodes { + id + title + genre + author { + id + name + } + } + } + } + """); + + // assert + result.MatchMarkdownSnapshot(); + } + + private static IServiceProvider CreateApplicationServices( + Action? configure = null) + { + var serviceCollection = new ServiceCollection(); + + serviceCollection + .AddSingleton() + .AddScoped() + .AddSingleton() + .AddSingleton(); + + serviceCollection + .AddGraphQLServer() + .AddCustomModule() + .AddGlobalObjectIdentification() + .AddMutationConventions(); + + configure?.Invoke(serviceCollection); + + return serviceCollection.BuildServiceProvider(); + } +} diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/InterceptsLocationAttribute.cs b/src/HotChocolate/Core/test/Types.Analyzers.Tests/InterceptsLocationAttribute.cs deleted file mode 100644 index 8b137891791..00000000000 --- a/src/HotChocolate/Core/test/Types.Analyzers.Tests/InterceptsLocationAttribute.cs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/Models/Author.cs b/src/HotChocolate/Core/test/Types.Analyzers.Tests/Models/Author.cs new file mode 100644 index 00000000000..a1b381f62d3 --- /dev/null +++ b/src/HotChocolate/Core/test/Types.Analyzers.Tests/Models/Author.cs @@ -0,0 +1,3 @@ +namespace HotChocolate.Types; + +public sealed record Author(int Id, string Name) : IEntity; diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/Models/AuthorAddress.cs b/src/HotChocolate/Core/test/Types.Analyzers.Tests/Models/AuthorAddress.cs new file mode 100644 index 00000000000..d5088b3548b --- /dev/null +++ b/src/HotChocolate/Core/test/Types.Analyzers.Tests/Models/AuthorAddress.cs @@ -0,0 +1,3 @@ +namespace HotChocolate.Types; + +public sealed record AuthorAddress(int Id, int AuthorId, string AuthorName, string Street, string City); diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/Models/Book.cs b/src/HotChocolate/Core/test/Types.Analyzers.Tests/Models/Book.cs new file mode 100644 index 00000000000..9cf4cb81bdc --- /dev/null +++ b/src/HotChocolate/Core/test/Types.Analyzers.Tests/Models/Book.cs @@ -0,0 +1,3 @@ +namespace HotChocolate.Types; + +public sealed record Book(int Id, string Title, int AuthorId, Genre Genre) : IEntity; diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/Models/Chapter.cs b/src/HotChocolate/Core/test/Types.Analyzers.Tests/Models/Chapter.cs new file mode 100644 index 00000000000..780b31584d7 --- /dev/null +++ b/src/HotChocolate/Core/test/Types.Analyzers.Tests/Models/Chapter.cs @@ -0,0 +1,3 @@ +namespace HotChocolate.Types; + +public record Chapter(int Number, string Title, int BookId); diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/Models/Genre.cs b/src/HotChocolate/Core/test/Types.Analyzers.Tests/Models/Genre.cs new file mode 100644 index 00000000000..048b488095e --- /dev/null +++ b/src/HotChocolate/Core/test/Types.Analyzers.Tests/Models/Genre.cs @@ -0,0 +1,8 @@ +namespace HotChocolate.Types; + +[EnumType("BookGenre")] +public enum Genre +{ + Fiction, + NonFiction +} diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/IEntity.cs b/src/HotChocolate/Core/test/Types.Analyzers.Tests/Models/IEntity.cs similarity index 59% rename from src/HotChocolate/Core/test/Types.Analyzers.Tests/IEntity.cs rename to src/HotChocolate/Core/test/Types.Analyzers.Tests/Models/IEntity.cs index 213d3786b6d..d4166b3e047 100644 --- a/src/HotChocolate/Core/test/Types.Analyzers.Tests/IEntity.cs +++ b/src/HotChocolate/Core/test/Types.Analyzers.Tests/Models/IEntity.cs @@ -1,7 +1,9 @@ +using HotChocolate.Types.Relay; + namespace HotChocolate.Types; [InterfaceType("Entity")] public interface IEntity { - string Name { get; } + [ID] int Id { get; } } diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/Person.cs b/src/HotChocolate/Core/test/Types.Analyzers.Tests/Person.cs deleted file mode 100644 index 0eef9d3545c..00000000000 --- a/src/HotChocolate/Core/test/Types.Analyzers.Tests/Person.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace HotChocolate.Types; - -[ObjectType] -public class Person : IEntity -{ - public int Id => 1; - - public string Name => default!; -} diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/PersonAddress.cs b/src/HotChocolate/Core/test/Types.Analyzers.Tests/PersonAddress.cs deleted file mode 100644 index 903494db636..00000000000 --- a/src/HotChocolate/Core/test/Types.Analyzers.Tests/PersonAddress.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System.Threading; -using System.Threading.Tasks; - -namespace HotChocolate.Types; - -[ExtendObjectType(typeof(Person))] -public class PersonAddress -{ - public Task GetAddressAsync( - AddressDataLoader dataLoader, - CancellationToken cancellationToken) - => dataLoader.LoadAsync("abc", cancellationToken); -} diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/PersonLastName.cs b/src/HotChocolate/Core/test/Types.Analyzers.Tests/PersonLastName.cs deleted file mode 100644 index 1b98ea99cff..00000000000 --- a/src/HotChocolate/Core/test/Types.Analyzers.Tests/PersonLastName.cs +++ /dev/null @@ -1,18 +0,0 @@ -using HotChocolate.Types.Relay; - -namespace HotChocolate.Types; - -[ObjectType] -public static partial class PersonLastName -{ - public static string LastName => default!; - - [Query] - public static string GetFooBarBaz() => "hello"; - - [NodeResolver] - public static Person GetPersonById(int id) - { - return new Person(); - } -} diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/RootTypeTests.cs b/src/HotChocolate/Core/test/Types.Analyzers.Tests/RootTypeTests.cs deleted file mode 100644 index 26073f61e28..00000000000 --- a/src/HotChocolate/Core/test/Types.Analyzers.Tests/RootTypeTests.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace HotChocolate.Types; - -public static class RootTypeTests -{ - [Query] - public static string Foo() => "foo"; - - [Mutation] - public static string Bar() => "bar"; -} \ No newline at end of file diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/Services/AddressByIdDataLoader.cs b/src/HotChocolate/Core/test/Types.Analyzers.Tests/Services/AddressByIdDataLoader.cs new file mode 100644 index 00000000000..70c09b37d44 --- /dev/null +++ b/src/HotChocolate/Core/test/Types.Analyzers.Tests/Services/AddressByIdDataLoader.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using GreenDonut; + +namespace HotChocolate.Types; + +public class AddressByIdDataLoader(IBatchScheduler batchScheduler, DataLoaderOptions options) + : BatchDataLoader(batchScheduler, options) +{ + private readonly Dictionary _addresses = new() + { + { 1, new AuthorAddress(1, 1, "Author 1", "Street 1", "City 1") }, + { 2, new AuthorAddress(2, 2, "Author 2", "Street 2", "City 2") }, + { 3, new AuthorAddress(3, 3, "Author 3", "Street 3", "City 3") } + }; + + protected override async Task> LoadBatchAsync( + IReadOnlyList keys, + CancellationToken cancellationToken) + { + await Task.Delay(10, cancellationToken); + return keys.ToDictionary(key => key, key => _addresses.TryGetValue(key, out var address) ? address : null); + } +} diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/Services/AuthorAddressRepository.cs b/src/HotChocolate/Core/test/Types.Analyzers.Tests/Services/AuthorAddressRepository.cs new file mode 100644 index 00000000000..1b0592b4d30 --- /dev/null +++ b/src/HotChocolate/Core/test/Types.Analyzers.Tests/Services/AuthorAddressRepository.cs @@ -0,0 +1,10 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace HotChocolate.Types; + +public sealed class AuthorAddressRepository(AddressByIdDataLoader addressById) +{ + public async Task GetAuthorAddressAsync(int authorId, CancellationToken cancellationToken) + => await addressById.LoadAsync(authorId, cancellationToken); +} diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/Services/AuthorRepository.cs b/src/HotChocolate/Core/test/Types.Analyzers.Tests/Services/AuthorRepository.cs new file mode 100644 index 00000000000..0b65e4cbddc --- /dev/null +++ b/src/HotChocolate/Core/test/Types.Analyzers.Tests/Services/AuthorRepository.cs @@ -0,0 +1,36 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace HotChocolate.Types; + +public sealed class AuthorRepository +{ + private readonly Dictionary _authors = new() + { + { 1, new Author(1, "Author 1") }, + { 2, new Author(2, "Author 2") }, + { 3, new Author(3, "Author 3") } + }; + + public Task GetAuthorAsync(int id, CancellationToken cancellationToken) + { + if (_authors.TryGetValue(id, out var author)) + { + return Task.FromResult(author); + } + + return Task.FromResult(null); + } + + public Task> GetAuthorsAsync(CancellationToken cancellationToken) + => Task.FromResult>(_authors.Values.OrderBy(t => t.Id)); + + public Task CreateAuthorAsync(string name, CancellationToken cancellationToken) + { + var author = new Author(_authors.Count + 1, name); + _authors.Add(author.Id, author); + return Task.FromResult(author); + } +} diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/Services/BookRepository.cs b/src/HotChocolate/Core/test/Types.Analyzers.Tests/Services/BookRepository.cs new file mode 100644 index 00000000000..2dca11de08e --- /dev/null +++ b/src/HotChocolate/Core/test/Types.Analyzers.Tests/Services/BookRepository.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace HotChocolate.Types; + +public sealed class BookRepository +{ + private readonly Dictionary _books = new() + { + { 1, new Book(1, "Book 1", 1, Genre.Fiction) }, + { 2, new Book(2, "Book 2", 2, Genre.NonFiction) }, + { 3, new Book(3, "Book 3", 3, Genre.NonFiction) } + }; + + public Task GetBookAsync(int id, CancellationToken cancellationToken) + => _books.TryGetValue(id, out var book) + ? Task.FromResult(book) + : Task.FromResult(null); + + public Task> GetBooksAsync(CancellationToken cancellationToken) + => Task.FromResult>(_books.Values.OrderBy(t => t.Id)); + + public Task> GetBooksByAuthorAsync(int authorId, CancellationToken cancellationToken) + => Task.FromResult>(_books.Values.Where(t => t.AuthorId == authorId).OrderBy(t => t.Id)); + + public Task CreateBookAsync(string title, Genre genre, int authorId, CancellationToken cancellationToken) + { + var book = new Book(_books.Count + 1, title, authorId, genre); + _books.Add(book.Id, book); + return Task.FromResult(book); + } +} diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/Services/ChapterRepository.cs b/src/HotChocolate/Core/test/Types.Analyzers.Tests/Services/ChapterRepository.cs new file mode 100644 index 00000000000..6d43f746761 --- /dev/null +++ b/src/HotChocolate/Core/test/Types.Analyzers.Tests/Services/ChapterRepository.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace HotChocolate.Types; + +public sealed class ChapterRepository +{ + private readonly Dictionary<(int, int), Chapter> _chapters = new() + { + { (1, 1), new Chapter(1, "Chapter 1", 1) }, + { (1, 2), new Chapter(2, "Chapter 2", 1) }, + { (2, 3), new Chapter(3, "Chapter 3", 2) } + }; + + public Task GetChapterAsync(int bookId, int chapterNumber, CancellationToken cancellationToken) + => _chapters.TryGetValue((bookId, chapterNumber), out var chapter) + ? Task.FromResult(chapter) + : Task.FromResult(null); + + public Task> GetChaptersAsync(int bookId, CancellationToken cancellationToken) + => Task.FromResult>(_chapters.Values.Where(t => t.BookId == bookId).OrderBy(t => t.Number)); + + public Task CreateChapterAsync(int bookId, string title, CancellationToken cancellationToken) + { + var chapter = new Chapter(_chapters.Count + 1, title, bookId); + _chapters.Add((bookId, chapter.Number), chapter); + return Task.FromResult(chapter); + } +} diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/Types/AddressType.cs b/src/HotChocolate/Core/test/Types.Analyzers.Tests/Types/AddressType.cs new file mode 100644 index 00000000000..6c8119c2f2e --- /dev/null +++ b/src/HotChocolate/Core/test/Types.Analyzers.Tests/Types/AddressType.cs @@ -0,0 +1,19 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace HotChocolate.Types; + +public class AddressType : ObjectType +{ + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Name("Address"); + } + + [Query] + public static async Task GetAddressByIdAsync( + int id, + AuthorAddressRepository repository, + CancellationToken cancellationToken) + => await repository.GetAuthorAddressAsync(id, cancellationToken); +} diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/Types/AuthorAddressExtension.cs b/src/HotChocolate/Core/test/Types.Analyzers.Tests/Types/AuthorAddressExtension.cs new file mode 100644 index 00000000000..df4aa377e75 --- /dev/null +++ b/src/HotChocolate/Core/test/Types.Analyzers.Tests/Types/AuthorAddressExtension.cs @@ -0,0 +1,14 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace HotChocolate.Types; + +[ExtendObjectType] +public static class AuthorAddressExtension +{ + public static Task GetAddressAsync( + [Parent]Author author, + AuthorAddressRepository repository, + CancellationToken cancellationToken) + => repository.GetAuthorAddressAsync(author.Id, cancellationToken); +} diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/Types/AuthorNode.cs b/src/HotChocolate/Core/test/Types.Analyzers.Tests/Types/AuthorNode.cs new file mode 100644 index 00000000000..1e537d7db95 --- /dev/null +++ b/src/HotChocolate/Core/test/Types.Analyzers.Tests/Types/AuthorNode.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace HotChocolate.Types; + +[ObjectType] +public static partial class AuthorNode +{ + [UsePaging] + public static async Task> GetBooksAsync( + [Parent] Author author, + BookRepository repository, + CancellationToken cancellationToken) + => await repository.GetBooksByAuthorAsync(author.Id, cancellationToken); + + + [Query] + public static string QueryFieldCollocatedWithAuthor() => "hello"; +} diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/Types/AuthorQueries.cs b/src/HotChocolate/Core/test/Types.Analyzers.Tests/Types/AuthorQueries.cs new file mode 100644 index 00000000000..8d2e7f2f9aa --- /dev/null +++ b/src/HotChocolate/Core/test/Types.Analyzers.Tests/Types/AuthorQueries.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using HotChocolate.Types.Relay; + +namespace HotChocolate.Types; + +[QueryType] +public static class AuthorQueries +{ + [UsePaging] + public static async Task> GetAuthorsAsync( + AuthorRepository repository, + CancellationToken cancellationToken) + => await repository.GetAuthorsAsync(cancellationToken); + + [NodeResolver] + public static async Task GetAuthorByIdAsync( + int id, + AuthorRepository repository, + CancellationToken cancellationToken) + => await repository.GetAuthorAsync(id, cancellationToken); +} diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/Types/BookNode.cs b/src/HotChocolate/Core/test/Types.Analyzers.Tests/Types/BookNode.cs new file mode 100644 index 00000000000..89bd55f844b --- /dev/null +++ b/src/HotChocolate/Core/test/Types.Analyzers.Tests/Types/BookNode.cs @@ -0,0 +1,41 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using HotChocolate.Types.Relay; + +namespace HotChocolate.Types; + +[ObjectType] +public static partial class BookNode +{ + static partial void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.BindFieldsExplicitly(); + descriptor.Field(t => t.Id); + descriptor.Field(t => t.Title); + descriptor.Field(t => t.Genre); + } + + public static async Task GetAuthorAsync( + [Parent] Book book, + AuthorRepository repository, + CancellationToken cancellationToken) + => await repository.GetAuthorAsync(book.AuthorId, cancellationToken); + + [UsePaging] + public static async Task> GetChapterAsync( + [Parent] Book book, + ChapterRepository repository, + CancellationToken cancellationToken) + => await repository.GetChaptersAsync(book.Id, cancellationToken); + + public static string IdAndTitle([Parent] Book book) + => $"{book.Id}: {book.Title}"; + + [NodeResolver] + public static async Task GetBookByIdAsync( + int id, + BookRepository repository, + CancellationToken cancellationToken) + => await repository.GetBookAsync(id, cancellationToken); +} diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/Types/BookOperations.cs b/src/HotChocolate/Core/test/Types.Analyzers.Tests/Types/BookOperations.cs new file mode 100644 index 00000000000..9d419762929 --- /dev/null +++ b/src/HotChocolate/Core/test/Types.Analyzers.Tests/Types/BookOperations.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace HotChocolate.Types; + +public static class BookOperations +{ + [Query] + [UsePaging] + public static async Task> GetBooksAsync( + BookRepository repository, + CancellationToken cancellationToken) + => await repository.GetBooksAsync(cancellationToken); + + [Query] + public static async Task GetBookByIdAsync( + int id, + BookRepository repository, + CancellationToken cancellationToken) + => await repository.GetBookAsync(id, cancellationToken); + + [Mutation] + public static async Task CreateBookAsync( + string title, + int authorId, + Genre genre, + BookRepository repository, + CancellationToken cancellationToken) + => await repository.CreateBookAsync(title, genre, authorId, cancellationToken); +} diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/Types/ChapterId.cs b/src/HotChocolate/Core/test/Types.Analyzers.Tests/Types/ChapterId.cs new file mode 100644 index 00000000000..e726651b81c --- /dev/null +++ b/src/HotChocolate/Core/test/Types.Analyzers.Tests/Types/ChapterId.cs @@ -0,0 +1,3 @@ +namespace HotChocolate.Types; + +public readonly record struct ChapterId(int Number, int BookId); diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/Types/ChapterIdSerializer.cs b/src/HotChocolate/Core/test/Types.Analyzers.Tests/Types/ChapterIdSerializer.cs new file mode 100644 index 00000000000..06b87440fa5 --- /dev/null +++ b/src/HotChocolate/Core/test/Types.Analyzers.Tests/Types/ChapterIdSerializer.cs @@ -0,0 +1,33 @@ +using System; +using HotChocolate.Types.Relay; + +namespace HotChocolate.Types; + +public sealed class ChapterIdSerializer : CompositeNodeIdValueSerializer +{ + protected override NodeIdFormatterResult Format(Span buffer, ChapterId value, out int written) + { + if (TryFormatIdPart(buffer, value.BookId, out var bookIdLength) && + TryFormatIdPart(buffer.Slice(bookIdLength), value.Number, out var numberLength)) + { + written = bookIdLength + numberLength; + return NodeIdFormatterResult.Success; + } + + written = 0; + return NodeIdFormatterResult.BufferTooSmall; + } + + protected override bool TryParse(ReadOnlySpan buffer, out ChapterId value) + { + if (TryParseIdPart(buffer, out int bookId, out var consumed) && + TryParseIdPart(buffer.Slice(consumed), out int chapterNumber, out _)) + { + value = new ChapterId(chapterNumber, bookId); + return true; + } + + value = default; + return false; + } +} diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/Types/ChapterNode.cs b/src/HotChocolate/Core/test/Types.Analyzers.Tests/Types/ChapterNode.cs new file mode 100644 index 00000000000..3609431ffb1 --- /dev/null +++ b/src/HotChocolate/Core/test/Types.Analyzers.Tests/Types/ChapterNode.cs @@ -0,0 +1,22 @@ +using System.Threading; +using System.Threading.Tasks; +using HotChocolate.Types.Relay; + +namespace HotChocolate.Types; + +[ObjectType] +public static partial class ChapterNode +{ + public static async Task GetBookAsync( + [Parent] Chapter chapter, + BookRepository repository, + CancellationToken cancellationToken) + => await repository.GetBookAsync(chapter.BookId, cancellationToken); + + [NodeResolver] + public static async Task GetChapterByIdAsync( + ChapterId id, + ChapterRepository repository, + CancellationToken cancellationToken) + => await repository.GetChapterAsync(id.BookId, id.Number, cancellationToken); +} diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/SomeRequestMiddleware.cs b/src/HotChocolate/Core/test/Types.Analyzers.Tests/Types/SomeRequestMiddleware.cs similarity index 100% rename from src/HotChocolate/Core/test/Types.Analyzers.Tests/SomeRequestMiddleware.cs rename to src/HotChocolate/Core/test/Types.Analyzers.Tests/Types/SomeRequestMiddleware.cs diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/StaticQueryExtension.cs b/src/HotChocolate/Core/test/Types.Analyzers.Tests/Types/StaticQueryExtension.cs similarity index 98% rename from src/HotChocolate/Core/test/Types.Analyzers.Tests/StaticQueryExtension.cs rename to src/HotChocolate/Core/test/Types.Analyzers.Tests/Types/StaticQueryExtension.cs index 86ef83ea43a..9ce0809d7a4 100644 --- a/src/HotChocolate/Core/test/Types.Analyzers.Tests/StaticQueryExtension.cs +++ b/src/HotChocolate/Core/test/Types.Analyzers.Tests/Types/StaticQueryExtension.cs @@ -6,4 +6,4 @@ namespace HotChocolate.Types; public static class StaticQueryExtension { public static string StaticField() => "foo"; -} \ No newline at end of file +} diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/___DemoCode.txt b/src/HotChocolate/Core/test/Types.Analyzers.Tests/___DemoCode.txt deleted file mode 100644 index 254cee1bd3a..00000000000 --- a/src/HotChocolate/Core/test/Types.Analyzers.Tests/___DemoCode.txt +++ /dev/null @@ -1,109 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using System.Threading.Tasks; -using HotChocolate.Resolvers; - -namespace HotChocolate.Types; - -internal static class Resolvers -{ - private readonly static ResolverInfo[] _resolverInfos = - [ - new ResolverInfo( - 0, - [new GenParameterInfo("id", typeof(string), []) ]) - ]; - private readonly static FieldResolverDelegate[] _fieldResolver = [Query_GetPersonById]; - private readonly static ParameterBinding[] _args_Query_GetPersonById = new ParameterBinding[1]; - private static bool _bindingsInitialized = false; - - public static void InitializeParameterBindings(IParameterBindingResolver parameterBindingResolver) - { - if (_bindingsInitialized) - { - return; - } - - foreach (var resolverInfo in _resolverInfos) - { - // we get the right binding collection for the resolver - var bindings = resolverInfo.Id switch - { - 1 => _args_Query_GetPersonById, - _ => throw new InvalidOperationException() - }; - - // next we resolve all bindings so that we can resolve the values for the resolver parameters. - for (var i = 0; i < resolverInfo.Parameters.Length; i++) - { - var parameter = resolverInfo.Parameters[i]; - bindings[i] = parameterBindingResolver.GetBinding(parameter); - } - } - } - - // example resolver - private static ValueTask Query_GetPersonById(IResolverContext context) - { - var args = MemoryMarshal.GetReference(_args_Query_GetPersonById.AsSpan()); - var arg0 = Unsafe.Add(ref args, 0).Resolve(context); - var result = PersonLastName.GetPersonById(arg0); - return new ValueTask(result); - } - - // generator proxies - public sealed class ResolverInfo(int id, ParameterInfo[] parameters) - { - public int Id { get; } = id; - - public ParameterInfo[] Parameters { get; } = parameters; - - public DescriptorAttributeInfo[] Attributes { get; } = []; - } - - public sealed class DescriptorAttributeInfo - { - public ICustomAttributeProvider Element { get; } - - public DescriptorAttribute Attribute { get; } - } - - public sealed class GenParameterInfo : ParameterInfo - { - public GenParameterInfo(string name, Type parameterType, CustomAttributeData[] customAttributes) - { - - } - - public override string? Name { get; } - - public override Type ParameterType => base.ParameterType; - - public override IEnumerable CustomAttributes => base.CustomAttributes; - - public override bool IsDefined(Type attributeType, bool inherit) - { - return base.IsDefined(attributeType, inherit); - } - } - - // this goes onto the core lib - public abstract class ParameterBinding - { - public abstract T Resolve(IResolverContext context); - } - - public sealed class ArgumentParameterBinding(string argumentName) : ParameterBinding - { - public override T Resolve(IResolverContext context) - => context.ArgumentValue(argumentName); - } - - public interface IParameterBindingResolver - { - ParameterBinding GetBinding(ParameterInfo parameter); - } -} diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/SchemaTests.ExecuteWithMiddleware.md b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/IntegrationTests.Class_Request_Middleware.md similarity index 69% rename from src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/SchemaTests.ExecuteWithMiddleware.md rename to src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/IntegrationTests.Class_Request_Middleware.md index 0e7bdfc12cb..789ef1759a2 100644 --- a/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/SchemaTests.ExecuteWithMiddleware.md +++ b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/IntegrationTests.Class_Request_Middleware.md @@ -1,4 +1,4 @@ -# ExecuteWithMiddleware +# Class_Request_Middleware ```json { diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/IntegrationTests.Query_Books_And_Authors.md b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/IntegrationTests.Query_Books_And_Authors.md new file mode 100644 index 00000000000..4422a3524a6 --- /dev/null +++ b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/IntegrationTests.Query_Books_And_Authors.md @@ -0,0 +1,39 @@ +# Query_Books_And_Authors + +```json +{ + "data": { + "books": { + "nodes": [ + { + "id": "Qm9vazox", + "title": "Book 1", + "genre": "FICTION", + "author": { + "id": "QXV0aG9yOjE=", + "name": "Author 1" + } + }, + { + "id": "Qm9vazoy", + "title": "Book 2", + "genre": "NON_FICTION", + "author": { + "id": "QXV0aG9yOjI=", + "name": "Author 2" + } + }, + { + "id": "Qm9vazoz", + "title": "Book 3", + "genre": "NON_FICTION", + "author": { + "id": "QXV0aG9yOjM=", + "name": "Author 3" + } + } + ] + } + } +} +``` diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/IntegrationTests.Schema.graphql b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/IntegrationTests.Schema.graphql new file mode 100644 index 00000000000..b1d6102b463 --- /dev/null +++ b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/IntegrationTests.Schema.graphql @@ -0,0 +1,144 @@ +schema { + query: Query + mutation: Mutation +} + +interface Entity { + id: ID! +} + +"The node interface is implemented by entities that have a global unique identifier." +interface Node { + id: ID! +} + +type Address { + id: Int! + authorId: Int! + authorName: String! + street: String! + city: String! +} + +type Author implements Entity & Node { + books("Returns the first _n_ elements from the list." first: Int "Returns the elements in the list that come after the specified cursor." after: String "Returns the last _n_ elements from the list." last: Int "Returns the elements in the list that come before the specified cursor." before: String): BooksConnection + id: ID! + name: String! + address: Address +} + +"A connection to a list of items." +type AuthorsConnection { + "Information to aid in pagination." + pageInfo: PageInfo! + "A list of edges." + edges: [AuthorsEdge!] + "A flattened list of the nodes." + nodes: [Author!] +} + +"An edge in a connection." +type AuthorsEdge { + "A cursor for use in pagination." + cursor: String! + "The item at the end of the edge." + node: Author! +} + +type Book implements Node & Entity { + id: ID! + author: Author + chapter("Returns the first _n_ elements from the list." first: Int "Returns the elements in the list that come after the specified cursor." after: String "Returns the last _n_ elements from the list." last: Int "Returns the elements in the list that come before the specified cursor." before: String): ChapterConnection + idAndTitle: String! + title: String! + genre: BookGenre! +} + +"A connection to a list of items." +type BooksConnection { + "Information to aid in pagination." + pageInfo: PageInfo! + "A list of edges." + edges: [BooksEdge!] + "A flattened list of the nodes." + nodes: [Book!] +} + +"An edge in a connection." +type BooksEdge { + "A cursor for use in pagination." + cursor: String! + "The item at the end of the edge." + node: Book! +} + +type Chapter implements Node { + id: ID! + book: Book + number: Int! + title: String! + bookId: Int! +} + +"A connection to a list of items." +type ChapterConnection { + "Information to aid in pagination." + pageInfo: PageInfo! + "A list of edges." + edges: [ChapterEdge!] + "A flattened list of the nodes." + nodes: [Chapter!] +} + +"An edge in a connection." +type ChapterEdge { + "A cursor for use in pagination." + cursor: String! + "The item at the end of the edge." + node: Chapter! +} + +type CreateBookPayload { + book: Book +} + +type Mutation { + createBook(input: CreateBookInput!): CreateBookPayload! +} + +"Information about pagination in a connection." +type PageInfo { + "Indicates whether more edges exist following the set defined by the clients arguments." + hasNextPage: Boolean! + "Indicates whether more edges exist prior the set defined by the clients arguments." + hasPreviousPage: Boolean! + "When paginating backwards, the cursor to continue." + startCursor: String + "When paginating forwards, the cursor to continue." + endCursor: String +} + +type Query { + "Fetches an object given its ID." + node("ID of the object." id: ID!): Node + "Lookup nodes by a list of IDs." + nodes("The list of node IDs." ids: [ID!]!): [Node]! + addressById(id: Int!): Address + queryFieldCollocatedWithAuthor: String! + books("Returns the first _n_ elements from the list." first: Int "Returns the elements in the list that come after the specified cursor." after: String "Returns the last _n_ elements from the list." last: Int "Returns the elements in the list that come before the specified cursor." before: String): BooksConnection + bookById(id: Int!): Book + authors("Returns the first _n_ elements from the list." first: Int "Returns the elements in the list that come after the specified cursor." after: String "Returns the last _n_ elements from the list." last: Int "Returns the elements in the list that come before the specified cursor." before: String): AuthorsConnection + authorById(id: ID!): Author + staticField: String! +} + +input CreateBookInput { + title: String! + authorId: Int! + genre: BookGenre! +} + +enum BookGenre { + FICTION + NON_FICTION +} \ No newline at end of file diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/SchemaTests.ExecuteRootField.md b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/SchemaTests.ExecuteRootField.md deleted file mode 100644 index 56e51f42871..00000000000 --- a/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/SchemaTests.ExecuteRootField.md +++ /dev/null @@ -1,9 +0,0 @@ -# ExecuteRootField - -```json -{ - "data": { - "foo": "foo" - } -} -``` diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/SchemaTests.SchemaSnapshot.md b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/SchemaTests.SchemaSnapshot.md deleted file mode 100644 index 9884aeda43a..00000000000 --- a/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/SchemaTests.SchemaSnapshot.md +++ /dev/null @@ -1,69 +0,0 @@ -# SchemaSnapshot - -```graphql -schema { - query: Query - mutation: Mutation - subscription: Subscription -} - -interface Entity { - name: String! -} - -"The node interface is implemented by entities that have a global unique identifier." -interface Node { - id: ID! -} - -type Author { - address: String! - name: String! -} - -type Mutation { - bar: String! - doSomething: String! -} - -type Person implements Node & Entity { - id: ID! - lastName: String! - name: String! - address: String! -} - -type Publisher { - company: String! - name: String! -} - -type Query { - "Fetches an object given its ID." - node("ID of the object." id: ID!): Node - "Lookup nodes by a list of IDs." - nodes("The list of node IDs." ids: [ID!]!): [Node]! - fooBarBaz: String! - foo: String! - person: Entity - enum: CustomEnum - book: SomeBook! - withDataLoader: String! - staticField: String! -} - -type SomeBook { - title: String - author: Author - publisher: Publisher -} - -type Subscription { - onSomething: String! -} - -enum CustomEnum { - ABC - DEF -} -``` diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/SomeQuery.cs b/src/HotChocolate/Core/test/Types.Analyzers.Tests/_remove/SomeQuery.txt similarity index 100% rename from src/HotChocolate/Core/test/Types.Analyzers.Tests/SomeQuery.cs rename to src/HotChocolate/Core/test/Types.Analyzers.Tests/_remove/SomeQuery.txt diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/SpecialObjectType.cs b/src/HotChocolate/Core/test/Types.Analyzers.Tests/_remove/SpecialObjectType.txt similarity index 100% rename from src/HotChocolate/Core/test/Types.Analyzers.Tests/SpecialObjectType.cs rename to src/HotChocolate/Core/test/Types.Analyzers.Tests/_remove/SpecialObjectType.txt