diff --git a/src/HotChocolate/Core/src/Abstractions/DataLoaderAttribute.cs b/src/HotChocolate/Core/src/Abstractions/DataLoaderAttribute.cs
index 5a304277ded..502c11c3107 100644
--- a/src/HotChocolate/Core/src/Abstractions/DataLoaderAttribute.cs
+++ b/src/HotChocolate/Core/src/Abstractions/DataLoaderAttribute.cs
@@ -7,17 +7,12 @@ namespace HotChocolate;
/// types source generator to generate necessary code around this method.
///
[AttributeUsage(AttributeTargets.Method)]
-public sealed class DataLoaderAttribute : Attribute
+public sealed class DataLoaderAttribute(string? name = null) : Attribute
{
- public DataLoaderAttribute(string? name = null)
- {
- Name = name;
- }
-
///
/// Gets the name override for the DataLoader or null.
///
- public string? Name { get; }
+ public string? Name { get; } = name;
///
/// Specifies how services by default are handled.
diff --git a/src/HotChocolate/Core/src/Abstractions/EventMessageAttribute.cs b/src/HotChocolate/Core/src/Abstractions/EventMessageAttribute.cs
index 572d433e18f..d15058702f4 100644
--- a/src/HotChocolate/Core/src/Abstractions/EventMessageAttribute.cs
+++ b/src/HotChocolate/Core/src/Abstractions/EventMessageAttribute.cs
@@ -3,7 +3,4 @@
namespace HotChocolate;
[AttributeUsage(AttributeTargets.Parameter)]
-public sealed class EventMessageAttribute
- : Attribute
-{
-}
+public sealed class EventMessageAttribute : Attribute;
\ No newline at end of file
diff --git a/src/HotChocolate/Core/src/Abstractions/MutationFieldAttribute.cs b/src/HotChocolate/Core/src/Abstractions/MutationFieldAttribute.cs
new file mode 100644
index 00000000000..299e8c2f769
--- /dev/null
+++ b/src/HotChocolate/Core/src/Abstractions/MutationFieldAttribute.cs
@@ -0,0 +1,6 @@
+using System;
+
+namespace HotChocolate;
+
+[AttributeUsage(AttributeTargets.Method | AttributeTargets.Property)]
+public sealed class MutationFieldAttribute : Attribute;
\ No newline at end of file
diff --git a/src/HotChocolate/Core/src/Abstractions/QueryFieldAttribute.cs b/src/HotChocolate/Core/src/Abstractions/QueryFieldAttribute.cs
new file mode 100644
index 00000000000..a49b8021eb2
--- /dev/null
+++ b/src/HotChocolate/Core/src/Abstractions/QueryFieldAttribute.cs
@@ -0,0 +1,6 @@
+using System;
+
+namespace HotChocolate;
+
+[AttributeUsage(AttributeTargets.Method | AttributeTargets.Property)]
+public sealed class QueryFieldAttribute : Attribute;
\ No newline at end of file
diff --git a/src/HotChocolate/Core/src/Abstractions/SubscriptionFieldAttribute.cs b/src/HotChocolate/Core/src/Abstractions/SubscriptionFieldAttribute.cs
new file mode 100644
index 00000000000..048a86d6e29
--- /dev/null
+++ b/src/HotChocolate/Core/src/Abstractions/SubscriptionFieldAttribute.cs
@@ -0,0 +1,6 @@
+using System;
+
+namespace HotChocolate;
+
+[AttributeUsage(AttributeTargets.Method | AttributeTargets.Property)]
+public sealed class SubscriptionFieldAttribute : Attribute;
\ No newline at end of file
diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Errors.cs b/src/HotChocolate/Core/src/Types.Analyzers/Errors.cs
new file mode 100644
index 00000000000..5ec5a06bd20
--- /dev/null
+++ b/src/HotChocolate/Core/src/Types.Analyzers/Errors.cs
@@ -0,0 +1,27 @@
+using HotChocolate.Types.Analyzers.Properties;
+using Microsoft.CodeAnalysis;
+
+namespace HotChocolate.Types.Analyzers;
+
+public static class Errors
+{
+ public static readonly DiagnosticDescriptor KeyParameterMissing =
+ new(
+ id: "HC0074",
+ title: "Parameter Missing.",
+ messageFormat:
+ SourceGenResources.DataLoader_KeyParameterMissing,
+ category: "DataLoader",
+ DiagnosticSeverity.Error,
+ isEnabledByDefault: true);
+
+ public static readonly DiagnosticDescriptor MethodAccessModifierInvalid =
+ new(
+ id: "HC0075",
+ title: "Access Modifier Invalid.",
+ messageFormat:
+ SourceGenResources.DataLoader_InvalidAccessModifier,
+ category: "DataLoader",
+ DiagnosticSeverity.Error,
+ isEnabledByDefault: true);
+}
\ No newline at end of file
diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Generators/DataLoaderGenerator.cs b/src/HotChocolate/Core/src/Types.Analyzers/Generators/DataLoaderGenerator.cs
deleted file mode 100644
index a46e7b03b96..00000000000
--- a/src/HotChocolate/Core/src/Types.Analyzers/Generators/DataLoaderGenerator.cs
+++ /dev/null
@@ -1,650 +0,0 @@
-using System.Text;
-using HotChocolate.Types.Analyzers.Helpers;
-using HotChocolate.Types.Analyzers.Inspectors;
-using Microsoft.CodeAnalysis;
-using Microsoft.CodeAnalysis.Text;
-using static System.StringComparison;
-using static HotChocolate.Types.Analyzers.Properties.SourceGenResources;
-using static HotChocolate.Types.Analyzers.StringConstants;
-using static HotChocolate.Types.Analyzers.WellKnownTypes;
-
-namespace HotChocolate.Types.Analyzers.Generators;
-
-public class DataLoaderGenerator : ISyntaxGenerator
-{
- private static readonly DiagnosticDescriptor _keyParameterMissing =
- new(
- id: "HC0074",
- title: "Parameter Missing.",
- messageFormat:
- DataLoader_KeyParameterMissing,
- category: "DataLoader",
- DiagnosticSeverity.Error,
- isEnabledByDefault: true);
-
- private static readonly DiagnosticDescriptor _methodAccessModifierInvalid =
- new(
- id: "HC0075",
- title: "Access Modifier Invalid.",
- messageFormat:
- DataLoader_InvalidAccessModifier,
- category: "DataLoader",
- DiagnosticSeverity.Error,
- isEnabledByDefault: true);
-
- public void Initialize(IncrementalGeneratorPostInitializationContext context) { }
-
- public bool Consume(ISyntaxInfo syntaxInfo)
- => syntaxInfo is DataLoaderInfo or ModuleInfo or DataLoaderDefaultsInfo;
-
- public void Generate(
- SourceProductionContext context,
- Compilation compilation,
- ReadOnlySpan syntaxInfos)
- {
- var (module, defaults) = syntaxInfos.GetDataLoaderDefaults(compilation.AssemblyName);
-
- var dataLoaders = new List();
- var sourceText = StringBuilderPool.Get();
-
- sourceText.AppendLine("// ");
- sourceText.AppendLine("#nullable enable");
- sourceText.AppendLine("using System;");
- sourceText.AppendLine("using Microsoft.Extensions.DependencyInjection;");
- sourceText.AppendLine("using HotChocolate.Execution.Configuration;");
-
- foreach (var syntaxInfo in syntaxInfos)
- {
- if (syntaxInfo is not DataLoaderInfo dataLoader)
- {
- continue;
- }
-
- if (dataLoader.MethodSymbol.Parameters.Length == 0)
- {
- context.ReportDiagnostic(
- Diagnostic.Create(
- _keyParameterMissing,
- Location.Create(
- dataLoader.MethodSyntax.SyntaxTree,
- dataLoader.MethodSyntax.ParameterList.Span)));
- continue;
- }
-
- if (dataLoader.MethodSymbol.DeclaredAccessibility is not Accessibility.Public
- and not Accessibility.Internal and not Accessibility.ProtectedAndInternal)
- {
- context.ReportDiagnostic(
- Diagnostic.Create(
- _methodAccessModifierInvalid,
- Location.Create(
- dataLoader.MethodSyntax.SyntaxTree,
- dataLoader.MethodSyntax.Modifiers.Span)));
- continue;
- }
-
- var keyArg = dataLoader.MethodSymbol.Parameters[0];
- var keyType = keyArg.Type;
- var cancellationTokenIndex = -1;
- var serviceMap = new Dictionary();
-
- if (IsKeysArgument(keyType))
- {
- keyType = ExtractKeyType(keyType);
- }
-
- InspectDataLoaderParameters(
- dataLoader,
- ref cancellationTokenIndex,
- serviceMap);
-
- DataLoaderKind kind;
-
- if (IsReturnTypeDictionary(dataLoader.MethodSymbol.ReturnType, keyType))
- {
- kind = DataLoaderKind.Batch;
- }
- else if (IsReturnTypeLookup(dataLoader.MethodSymbol.ReturnType, keyType))
- {
- kind = DataLoaderKind.Group;
- }
- else
- {
- keyType = keyArg.Type;
- kind = DataLoaderKind.Cache;
- }
-
- var valueType = ExtractValueType(dataLoader.MethodSymbol.ReturnType, kind);
-
- dataLoaders.Add(dataLoader);
-
- GenerateDataLoader(
- dataLoader,
- defaults,
- kind,
- keyType,
- valueType,
- dataLoader.MethodSymbol.Parameters.Length,
- cancellationTokenIndex,
- serviceMap,
- sourceText);
- }
-
- // if we find no valid DataLoader we will not create any file.
- if (dataLoaders.Count > 0)
- {
- if (defaults.RegisterServices)
- {
- // write DI integration
- sourceText.AppendLine();
- sourceText.AppendLine("namespace Microsoft.Extensions.DependencyInjection");
- sourceText.AppendLine("{");
- GenerateDataLoaderRegistrations(module, dataLoaders, sourceText);
- sourceText.AppendLine("}");
- }
-
- context.AddSource(
- WellKnownFileNames.DataLoaderFile,
- SourceText.From(sourceText.ToString(), Encoding.UTF8));
- }
-
- StringBuilderPool.Return(sourceText);
- }
-
- private static void GenerateDataLoader(
- DataLoaderInfo dataLoader,
- DataLoaderDefaultsInfo defaults,
- DataLoaderKind kind,
- ITypeSymbol keyType,
- ITypeSymbol valueType,
- int parameterCount,
- int cancelIndex,
- Dictionary services,
- StringBuilder sourceText)
- {
- var isScoped = dataLoader.IsScoped ?? defaults.Scoped ?? false;
- var isPublic = dataLoader.IsPublic ?? defaults.IsPublic ?? true;
- var isInterfacePublic = dataLoader.IsInterfacePublic ?? defaults.IsInterfacePublic ?? true;
-
- sourceText.AppendLine();
- sourceText.Append("namespace ");
- sourceText.AppendLine(dataLoader.Namespace);
- sourceText.AppendLine("{");
-
- // first we generate a DataLoader interface ...
- var interfaceName = dataLoader.InterfaceName;
-
- if (isInterfacePublic)
- {
- sourceText.Append(" public interface ");
- }
- else
- {
- sourceText.Append(" internal interface ");
- }
-
- sourceText.Append(interfaceName);
-
- if (kind is DataLoaderKind.Batch or DataLoaderKind.Cache)
- {
- sourceText.Append(" : global::GreenDonut.IDataLoader<");
- sourceText.Append(keyType.ToFullyQualified());
- sourceText.Append(", ");
- sourceText.Append(valueType.ToFullyQualified());
- sourceText.Append(">");
- }
- else if (kind is DataLoaderKind.Group)
- {
- sourceText.Append(" : global::GreenDonut.IDataLoader<");
- sourceText.Append(keyType.ToFullyQualified());
- sourceText.Append(", ");
- sourceText.Append(valueType.ToFullyQualified());
- sourceText.Append("[]>");
- }
-
- sourceText.AppendLine(" { }");
- sourceText.AppendLine();
-
- // ... then the actual DataLoader implementation.
- if (isPublic)
- {
- sourceText.Append(" public sealed class ");
- }
- else
- {
- sourceText.Append(" internal sealed class ");
- }
-
- sourceText.Append(dataLoader.Name);
-
- if (kind is DataLoaderKind.Batch)
- {
- sourceText.Append(" : global::GreenDonut.BatchDataLoader<");
- sourceText.Append(keyType.ToFullyQualified());
- sourceText.Append(", ");
- sourceText.Append(valueType.ToFullyQualified());
- sourceText.Append(">");
- }
- else if (kind is DataLoaderKind.Group)
- {
- sourceText.Append(" : global::GreenDonut.GroupedDataLoader<");
- sourceText.Append(keyType.ToFullyQualified());
- sourceText.Append(", ");
- sourceText.Append(valueType.ToFullyQualified());
- sourceText.Append(">");
- }
- else if (kind is DataLoaderKind.Cache)
- {
- sourceText.Append(" : global::GreenDonut.CacheDataLoader<");
- sourceText.Append(keyType.ToFullyQualified());
- sourceText.Append(", ");
- sourceText.Append(valueType.ToFullyQualified());
- sourceText.Append(">");
- }
-
- sourceText.Append(", ");
- sourceText.AppendLine(interfaceName);
- sourceText.AppendLine(" {");
-
- sourceText.AppendLine(
- " private readonly global::System.IServiceProvider _services;");
- sourceText.AppendLine();
-
- if (kind is DataLoaderKind.Batch or DataLoaderKind.Group)
- {
- sourceText
- .Append(Indent)
- .Append(Indent)
- .Append("public ")
- .Append(dataLoader.Name)
- .AppendLine("(");
- sourceText
- .Append(Indent)
- .Append(Indent)
- .Append(Indent)
- .AppendLine("global::System.IServiceProvider services,");
- sourceText
- .Append(Indent)
- .Append(Indent)
- .Append(Indent)
- .AppendLine("global::GreenDonut.IBatchScheduler batchScheduler,");
- sourceText
- .Append(Indent)
- .Append(Indent)
- .Append(Indent)
- .AppendLine("global::GreenDonut.DataLoaderOptions? options = null)");
-
- sourceText
- .Append(Indent)
- .Append(Indent)
- .Append(Indent)
- .AppendLine(": base(batchScheduler, options)");
- }
- else
- {
- sourceText
- .Append(Indent)
- .Append(Indent)
- .Append("public ")
- .Append(dataLoader.Name)
- .AppendLine("(");
-
- sourceText
- .Append(Indent)
- .Append(Indent)
- .Append(Indent)
- .AppendLine("global::System.IServiceProvider services,");
-
- sourceText
- .Append(Indent)
- .Append(Indent)
- .Append(Indent)
- .AppendLine("global::GreenDonut.DataLoaderOptions? options = null)");
-
- sourceText
- .Append(Indent)
- .Append(Indent)
- .Append(Indent)
- .AppendLine(": base(options)");
- }
-
- sourceText.AppendLine(" {");
- sourceText.AppendLine(" _services = services ??");
- sourceText.Append(" throw new global::")
- .AppendLine("System.ArgumentNullException(nameof(services));");
- sourceText.AppendLine(" }");
- sourceText.AppendLine();
-
- if (kind is DataLoaderKind.Batch)
- {
- sourceText.Append($" protected override async global::{WellKnownTypes.Task}<");
- sourceText.Append($"{ReadOnlyDictionary}<");
- sourceText.Append(keyType.ToFullyQualified());
- sourceText.Append(", ");
- sourceText.Append(valueType.ToFullyQualified());
- sourceText.Append(">> ");
- sourceText.AppendLine("LoadBatchAsync(");
- sourceText.Append($" {ReadOnlyList}<");
- sourceText.Append(keyType.ToFullyQualified()).AppendLine("> keys,");
- sourceText.AppendLine($" global::{WellKnownTypes.CancellationToken} ct)");
- }
- else if (kind is DataLoaderKind.Group)
- {
- sourceText.Append($" protected override async global::{WellKnownTypes.Task}<");
- sourceText.Append($"{Lookup}<");
- sourceText.Append(keyType.ToFullyQualified());
- sourceText.Append(", ");
- sourceText.Append(valueType.ToFullyQualified());
- sourceText.Append(">> ");
- sourceText.AppendLine("LoadGroupedBatchAsync(");
- sourceText.Append($" {ReadOnlyList}<");
- sourceText.Append(keyType.ToFullyQualified()).AppendLine("> keys,");
- sourceText.AppendLine($" global::{WellKnownTypes.CancellationToken} ct)");
- }
- else if (kind is DataLoaderKind.Cache)
- {
- sourceText.Append($" protected override async global::{WellKnownTypes.Task}<");
- sourceText.Append(valueType.ToFullyQualified());
- sourceText.Append("> ");
- sourceText.AppendLine("LoadSingleAsync(");
- sourceText
- .Append(" ")
- .Append(keyType.ToFullyQualified())
- .AppendLine(" key,");
- sourceText.AppendLine($" {WellKnownTypes.CancellationToken} ct)");
- }
-
- sourceText.AppendLine(" {");
-
- if (isScoped)
- {
- sourceText
- .Append(Indent)
- .Append(Indent)
- .Append(Indent)
- .AppendLine("await using var scope = _services.CreateAsyncScope();");
-
- foreach (var item in services.OrderBy(t => t.Key))
- {
- sourceText.Append(" var p").Append(item.Key).Append(" = ");
- sourceText.Append("scope.ServiceProvider.GetRequiredService<");
- sourceText.Append(item.Value);
- sourceText.AppendLine(">();");
- }
- }
- else
- {
- foreach (var item in services.OrderBy(t => t.Key))
- {
- sourceText
- .Append(" var p")
- .Append(item.Key)
- .Append(" = _services.GetRequiredService<");
- sourceText.Append(item.Value);
- sourceText.AppendLine(">();");
- }
- }
-
- sourceText.Append(" return await ");
- sourceText.Append(dataLoader.ContainingType);
- sourceText.Append(".");
- sourceText.Append(dataLoader.MethodName);
- sourceText.Append("(");
-
- for (var i = 0; i < parameterCount; i++)
- {
- if (i > 0)
- {
- sourceText.Append(", ");
- }
-
- if (i == 0)
- {
- if (kind is DataLoaderKind.Batch or DataLoaderKind.Group)
- {
- sourceText.Append("keys");
- }
- else
- {
- sourceText.Append("key");
- }
- }
- else if (i == cancelIndex)
- {
- sourceText.Append("ct");
- }
- else
- {
- sourceText.Append("p");
- sourceText.Append(i);
- }
- }
- sourceText.AppendLine(").ConfigureAwait(false);");
-
- sourceText.AppendLine(" }");
- sourceText.AppendLine(" }");
- sourceText.AppendLine("}");
- }
-
- private static void GenerateDataLoaderRegistrations(
- ModuleInfo module,
- List dataLoaders,
- StringBuilder sourceText)
- {
- sourceText.Append(Indent)
- .Append("public static partial class ")
- .Append(module.ModuleName)
- .AppendLine("RequestExecutorBuilderExtensions");
-
- sourceText
- .Append(Indent)
- .AppendLine("{");
-
- sourceText
- .Append(Indent)
- .Append(Indent)
- .Append("static partial void RegisterGeneratedDataLoader(")
- .AppendLine("IRequestExecutorBuilder builder)");
-
- sourceText
- .Append(Indent)
- .Append(Indent)
- .AppendLine("{");
-
- foreach (var dataLoader in dataLoaders)
- {
- sourceText
- .Append(Indent)
- .Append(Indent)
- .Append(Indent)
- .Append("builder.AddDataLoader<")
- .Append(dataLoader.InterfaceFullName)
- .Append(", ")
- .Append(dataLoader.FullName)
- .AppendLine(">();");
- }
-
- sourceText
- .Append(Indent)
- .Append(Indent)
- .AppendLine("}");
-
- sourceText
- .Append(Indent)
- .AppendLine("}");
- }
-
- private void InspectDataLoaderParameters(
- DataLoaderInfo dataLoader,
- ref int cancellationTokenIndex,
- Dictionary serviceMap)
- {
- for (var i = 1; i < dataLoader.MethodSymbol.Parameters.Length; i++)
- {
- var argument = dataLoader.MethodSymbol.Parameters[i];
- var argumentType = argument.Type.ToFullyQualified();
-
- if (IsCancellationToken(argumentType))
- {
- if (cancellationTokenIndex != -1)
- {
- // report error
- return;
- }
-
- cancellationTokenIndex = i;
- }
- else
- {
- serviceMap[i] = argumentType;
- }
- }
- }
-
- private static bool IsKeysArgument(ITypeSymbol type)
- => type is INamedTypeSymbol { IsGenericType: true, TypeArguments.Length: 1, } nt &&
- ReadOnlyList.Equals(ToTypeNameNoGenerics(nt), Ordinal);
-
- private static ITypeSymbol ExtractKeyType(ITypeSymbol type)
- {
- if (type is INamedTypeSymbol { IsGenericType: true, TypeArguments.Length: 1, } namedType &&
- ReadOnlyList.Equals(ToTypeNameNoGenerics(namedType), Ordinal))
- {
- return namedType.TypeArguments[0];
- }
-
- throw new InvalidOperationException();
- }
-
- private static bool IsCancellationToken(string typeName)
- => string.Equals(typeName, WellKnownTypes.CancellationToken) ||
- string.Equals(typeName, GlobalCancellationToken);
-
- private static bool IsReturnTypeDictionary(ITypeSymbol returnType, ITypeSymbol keyType)
- {
- if (returnType is INamedTypeSymbol { TypeArguments.Length: 1, } namedType)
- {
- var resultType = namedType.TypeArguments[0];
-
- if (IsReadOnlyDictionary(resultType) &&
- resultType is INamedTypeSymbol { TypeArguments.Length: 2, } dictionaryType &&
- dictionaryType.TypeArguments[0].Equals(keyType, SymbolEqualityComparer.Default))
- {
- return true;
- }
- }
- return false;
- }
-
- private static bool IsReturnTypeLookup(ITypeSymbol returnType, ITypeSymbol keyType)
- {
- if (returnType is INamedTypeSymbol { TypeArguments.Length: 1, } namedType)
- {
- var resultType = namedType.TypeArguments[0];
-
- if (ToTypeNameNoGenerics(resultType).Equals(Lookup, Ordinal) &&
- resultType is INamedTypeSymbol { TypeArguments.Length: 2, } dictionaryType &&
- dictionaryType.TypeArguments[0].Equals(keyType, SymbolEqualityComparer.Default))
- {
- return true;
- }
- }
- return false;
- }
-
- private static bool IsReadOnlyDictionary(ITypeSymbol type)
- {
- if (!ToTypeNameNoGenerics(type).Equals(ReadOnlyDictionary, Ordinal))
- {
- foreach (var interfaceSymbol in type.Interfaces)
- {
- if (ToTypeNameNoGenerics(interfaceSymbol).Equals(ReadOnlyDictionary, Ordinal))
- {
- return true;
- }
- }
-
- return false;
- }
-
- return true;
- }
-
- private static ITypeSymbol ExtractValueType(ITypeSymbol returnType, DataLoaderKind kind)
- {
- if (returnType is INamedTypeSymbol { TypeArguments.Length: 1, } namedType)
- {
- if (kind is DataLoaderKind.Batch or DataLoaderKind.Group &&
- namedType.TypeArguments[0] is INamedTypeSymbol { TypeArguments.Length: 2, } dict)
- {
- return dict.TypeArguments[1];
- }
-
- if (kind is DataLoaderKind.Cache)
- {
- return namedType.TypeArguments[0];
- }
- }
-
- throw new InvalidOperationException();
- }
-
- private static string ToTypeNameNoGenerics(ITypeSymbol typeSymbol)
- => $"{typeSymbol.ContainingNamespace}.{typeSymbol.Name}";
-}
-
-internal static class GeneratorUtils
-{
- public static ModuleInfo GetModuleInfo(
- this ReadOnlySpan syntaxInfos,
- string? assemblyName,
- out bool defaultModule)
- {
- foreach (var syntaxInfo in syntaxInfos)
- {
- if (syntaxInfo is ModuleInfo module)
- {
- defaultModule = false;
- return module;
- }
- }
-
- defaultModule = true;
- return new ModuleInfo(CreateModuleName(assemblyName), ModuleOptions.Default);
- }
-
- public static (ModuleInfo, DataLoaderDefaultsInfo) GetDataLoaderDefaults(
- this ReadOnlySpan syntaxInfos,
- string? assemblyName)
- {
- ModuleInfo? moduleInfo = null;
- DataLoaderDefaultsInfo? dataLoaderDefaultsInfo = null;
-
- foreach (var syntaxInfo in syntaxInfos)
- {
- if (moduleInfo is null && syntaxInfo is ModuleInfo mi)
- {
- moduleInfo = mi;
- }
- else if (dataLoaderDefaultsInfo is null && syntaxInfo is DataLoaderDefaultsInfo dldi)
- {
- dataLoaderDefaultsInfo = dldi;
- }
-
- if (moduleInfo is not null && dataLoaderDefaultsInfo is not null)
- {
- return (moduleInfo, dataLoaderDefaultsInfo);
- }
- }
-
- moduleInfo ??= new ModuleInfo(CreateModuleName(assemblyName), ModuleOptions.Default);
- dataLoaderDefaultsInfo ??= new DataLoaderDefaultsInfo(null, null, true, true);
-
- return (moduleInfo, dataLoaderDefaultsInfo);
- }
-
- private static string CreateModuleName(string? assemblyName)
- => assemblyName is null
- ? "AssemblyTypes"
- : assemblyName.Split('.').Last() + "Types";
-}
\ No newline at end of file
diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Generators/DataLoaderSyntaxGenerator.cs b/src/HotChocolate/Core/src/Types.Analyzers/Generators/DataLoaderSyntaxGenerator.cs
new file mode 100644
index 00000000000..0e5b69474e9
--- /dev/null
+++ b/src/HotChocolate/Core/src/Types.Analyzers/Generators/DataLoaderSyntaxGenerator.cs
@@ -0,0 +1,301 @@
+using System.Text;
+using HotChocolate.Types.Analyzers.Helpers;
+using HotChocolate.Types.Analyzers.Inspectors;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.Text;
+
+namespace HotChocolate.Types.Analyzers.Generators;
+
+public sealed class DataLoaderSyntaxGenerator : IDisposable
+{
+ private StringBuilder _sb;
+ private CodeWriter _writer;
+ private bool _disposed;
+
+ public DataLoaderSyntaxGenerator()
+ {
+ _sb = StringBuilderPool.Get();
+ _writer = new CodeWriter(_sb);
+ }
+
+ public void WriterHeader()
+ {
+ _writer.WriteFileHeader();
+ _writer.WriteIndentedLine("using Microsoft.Extensions.DependencyInjection;");
+ _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 void WriteDataLoaderInterface(
+ string name,
+ bool isPublic,
+ DataLoaderKind kind,
+ ITypeSymbol key,
+ ITypeSymbol value)
+ {
+ _writer.WriteIndentedLine(
+ "{0} interface {1}",
+ isPublic
+ ? "public"
+ : "internal",
+ name);
+ _writer.IncreaseIndent();
+
+ _writer.WriteIndentedLine(
+ kind is DataLoaderKind.Group
+ ? ": global::GreenDonut.IDataLoader<{0}, {1}[]>"
+ : ": global::GreenDonut.IDataLoader<{0}, {1}>",
+ key.ToFullyQualified(),
+ value.ToFullyQualified());
+
+ _writer.DecreaseIndent();
+ _writer.WriteIndentedLine("{");
+ _writer.WriteIndentedLine("}");
+ _writer.WriteLine();
+ }
+
+ public void WriteBeginDataLoaderClass(
+ string name,
+ string interfaceName,
+ bool isPublic,
+ DataLoaderKind kind,
+ ITypeSymbol key,
+ ITypeSymbol value)
+ {
+ _writer.WriteIndentedLine(
+ "{0} sealed class {1}",
+ isPublic
+ ? "public"
+ : "internal",
+ name);
+ _writer.IncreaseIndent();
+
+ switch (kind)
+ {
+ case DataLoaderKind.Batch:
+ _writer.WriteIndentedLine(
+ ": global::GreenDonut.BatchDataLoader<{0}, {1}>",
+ key.ToFullyQualified(),
+ value.ToFullyQualified());
+ break;
+
+ case DataLoaderKind.Group:
+ _writer.WriteIndentedLine(
+ ": global::GreenDonut.GroupedDataLoader<{0}, {1}>",
+ key.ToFullyQualified(),
+ value.ToFullyQualified());
+ break;
+
+ case DataLoaderKind.Cache:
+ _writer.WriteIndentedLine(
+ ": global::GreenDonut.CacheDataLoader<{0}, {1}>",
+ key.ToFullyQualified(),
+ value.ToFullyQualified());
+ break;
+ }
+
+ _writer.WriteIndentedLine(", {0}", interfaceName);
+ _writer.DecreaseIndent();
+ _writer.WriteIndentedLine("{");
+ _writer.IncreaseIndent();
+ }
+
+ public void WriteEndDataLoaderClass()
+ {
+ _writer.DecreaseIndent();
+ _writer.WriteIndentedLine("}");
+ _writer.WriteLine();
+ }
+
+ public void WriteDataLoaderConstructor(
+ string name,
+ DataLoaderKind kind)
+ {
+ _writer.WriteIndentedLine("private readonly global::System.IServiceProvider _services;");
+ _writer.WriteLine();
+
+ if (kind is DataLoaderKind.Batch or DataLoaderKind.Group)
+ {
+ _writer.WriteIndentedLine("public {0}(", name);
+
+ using (_writer.IncreaseIndent())
+ {
+ _writer.WriteIndentedLine("global::System.IServiceProvider services,");
+ _writer.WriteIndentedLine("global::GreenDonut.IBatchScheduler batchScheduler,");
+ _writer.WriteIndentedLine("global::GreenDonut.DataLoaderOptions? options = null)");
+ _writer.WriteIndentedLine(": base(batchScheduler, options)");
+ }
+ }
+ else
+ {
+ _writer.WriteIndentedLine("public {0}(", name);
+
+ using (_writer.IncreaseIndent())
+ {
+ _writer.WriteIndentedLine("global::System.IServiceProvider services,");
+ _writer.WriteIndentedLine("global::GreenDonut.DataLoaderOptions? options = null)");
+ _writer.WriteIndentedLine(": base(options)");
+ }
+ }
+
+ _writer.WriteIndentedLine("{");
+
+ using (_writer.IncreaseIndent())
+ {
+ _writer.WriteIndentedLine("_services = services ??");
+
+ using (_writer.IncreaseIndent())
+ {
+ _writer.WriteIndentedLine("throw new global::System.ArgumentNullException(nameof(services));");
+ }
+ }
+
+ _writer.WriteIndentedLine("}");
+ }
+
+ public void WriteDataLoaderLoadMethod(
+ string containingType,
+ string methodName,
+ bool isScoped,
+ DataLoaderKind kind,
+ ITypeSymbol key,
+ ITypeSymbol value,
+ Dictionary services,
+ int parameterCount,
+ int cancelIndex)
+ {
+ if (kind is DataLoaderKind.Batch)
+ {
+ _writer.WriteIndentedLine(
+ "protected override async global::{0}<{1}<{2}, {3}>> LoadBatchAsync(",
+ WellKnownTypes.Task,
+ WellKnownTypes.ReadOnlyDictionary,
+ key.ToFullyQualified(),
+ value.ToFullyQualified());
+
+ using (_writer.IncreaseIndent())
+ {
+ _writer.WriteIndentedLine("{0}<{1}> keys,", WellKnownTypes.ReadOnlyList, key.ToFullyQualified());
+ _writer.WriteIndentedLine("global::{0} ct)", WellKnownTypes.CancellationToken);
+ }
+ }
+ else if (kind is DataLoaderKind.Group)
+ {
+ _writer.WriteIndentedLine(
+ "protected override async global::{0}<{1}<{2}, {3}>> LoadGroupedBatchAsync(",
+ WellKnownTypes.Task,
+ WellKnownTypes.Lookup,
+ key.ToFullyQualified(),
+ value.ToFullyQualified());
+
+ using (_writer.IncreaseIndent())
+ {
+ _writer.WriteIndentedLine("{0}<{1}> keys,", WellKnownTypes.ReadOnlyList, key.ToFullyQualified());
+ _writer.WriteIndentedLine("global::{0} ct)", WellKnownTypes.CancellationToken);
+ }
+ }
+ else if (kind is DataLoaderKind.Cache)
+ {
+ _writer.WriteIndentedLine(
+ "protected override async global::{0}<{1}> LoadSingleAsync(",
+ WellKnownTypes.Task,
+ value.ToFullyQualified());
+
+ using (_writer.IncreaseIndent())
+ {
+ _writer.WriteIndentedLine("{0} key,", key.ToFullyQualified());
+ _writer.WriteIndentedLine("global::{0} ct)", WellKnownTypes.CancellationToken);
+ }
+ }
+
+ _writer.WriteIndentedLine("{");
+
+ using (_writer.IncreaseIndent())
+ {
+ if (isScoped)
+ {
+ _writer.WriteIndentedLine("await using var scope = _services.CreateAsyncScope();");
+
+ foreach (var item in services.OrderBy(t => t.Key))
+ {
+ _writer.WriteIndentedLine(
+ "var p{0} = scope.ServiceProvider.GetRequiredService<{1}>();",
+ item.Key,
+ item.Value);
+ }
+ }
+ else
+ {
+ foreach (var item in services.OrderBy(t => t.Key))
+ {
+ _writer.WriteIndentedLine(
+ "var p{0} = _services.GetRequiredService<{1}>();",
+ item.Key,
+ item.Value);
+ }
+ }
+
+ _writer.WriteIndented("return await {0}.{1}(", containingType, methodName);
+
+ for (var i = 0; i < parameterCount; i++)
+ {
+ if (i > 0)
+ {
+ _writer.Write(", ");
+ }
+
+ if (i == 0)
+ {
+ _writer.Write(
+ kind is DataLoaderKind.Cache
+ ? "key"
+ : "keys");
+ }
+ else if (i == cancelIndex)
+ {
+ _writer.Write("ct");
+ }
+ else
+ {
+ _writer.Write("p");
+ _writer.Write(i);
+ }
+ }
+ _writer.WriteLine(").ConfigureAwait(false);");
+ }
+
+ _writer.WriteIndentedLine("}");
+ }
+
+ public override string ToString()
+ => _sb.ToString();
+
+ public SourceText ToSourceText()
+ => SourceText.From(ToString(), Encoding.UTF8);
+
+ public void Dispose()
+ {
+ if (_disposed)
+ {
+ return;
+ }
+
+ StringBuilderPool.Return(_sb);
+ _sb = default!;
+ _writer = default!;
+ _disposed = true;
+ }
+}
\ No newline at end of file
diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Generators/ISyntaxGenerator.cs b/src/HotChocolate/Core/src/Types.Analyzers/Generators/ISyntaxGenerator.cs
deleted file mode 100644
index 46fef62a445..00000000000
--- a/src/HotChocolate/Core/src/Types.Analyzers/Generators/ISyntaxGenerator.cs
+++ /dev/null
@@ -1,29 +0,0 @@
-using HotChocolate.Types.Analyzers.Inspectors;
-using Microsoft.CodeAnalysis;
-
-namespace HotChocolate.Types.Analyzers.Generators;
-
-///
-/// A syntax generator produces C# code from the consumed syntax infos.
-///
-public interface ISyntaxGenerator
-{
- ///
- /// Allows to create initial code like attributes.
- ///
- ///
- void Initialize(IncrementalGeneratorPostInitializationContext context);
-
- ///
- /// Specifies if the given will be consumed by this generator.
- ///
- bool Consume(ISyntaxInfo syntaxInfo);
-
- ///
- /// Generates the C# source code for the consumed syntax infos.
- ///
- void Generate(
- SourceProductionContext context,
- Compilation compilation,
- ReadOnlySpan consumed);
-}
diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Generators/ModuleGenerator.cs b/src/HotChocolate/Core/src/Types.Analyzers/Generators/ModuleGenerator.cs
deleted file mode 100644
index facf45e114d..00000000000
--- a/src/HotChocolate/Core/src/Types.Analyzers/Generators/ModuleGenerator.cs
+++ /dev/null
@@ -1,200 +0,0 @@
-using System.Text;
-using HotChocolate.Types.Analyzers.Helpers;
-using HotChocolate.Types.Analyzers.Inspectors;
-using Microsoft.CodeAnalysis;
-using Microsoft.CodeAnalysis.Text;
-using static HotChocolate.Types.Analyzers.StringConstants;
-using static HotChocolate.Types.Analyzers.WellKnownFileNames;
-using TypeInfo = HotChocolate.Types.Analyzers.Inspectors.TypeInfo;
-
-namespace HotChocolate.Types.Analyzers.Generators;
-
-public class ModuleGenerator : ISyntaxGenerator
-{
- public void Initialize(IncrementalGeneratorPostInitializationContext context)
- {
- }
-
- public bool Consume(ISyntaxInfo syntaxInfo)
- => syntaxInfo is TypeInfo or TypeExtensionInfo or
- RegisterDataLoaderInfo or ModuleInfo or DataLoaderInfo;
-
- public void Generate(
- SourceProductionContext context,
- Compilation compilation,
- ReadOnlySpan syntaxInfos)
- {
- var module = syntaxInfos.GetModuleInfo(compilation.AssemblyName, out var defaultModule);
-
- // if there is only the module info we do not need to generate a module.
- if (!defaultModule && syntaxInfos.Length == 1)
- {
- return;
- }
-
- var sourceText = StringBuilderPool.Get();
- sourceText.AppendLine("// ");
- sourceText.AppendLine("#nullable enable");
- sourceText.AppendLine("using System;");
- sourceText.AppendLine("using HotChocolate.Execution.Configuration;");
-
- sourceText.AppendLine();
- sourceText.AppendLine("namespace Microsoft.Extensions.DependencyInjection");
- sourceText.AppendLine("{");
-
- sourceText.Append(Indent)
- .Append("public static partial class ")
- .Append(module.ModuleName)
- .AppendLine("RequestExecutorBuilderExtensions");
-
- sourceText.Append(Indent)
- .AppendLine("{");
-
- sourceText.Append(Indent)
- .Append(Indent)
- .Append("public static IRequestExecutorBuilder Add")
- .Append(module.ModuleName)
- .AppendLine("(this IRequestExecutorBuilder builder)");
-
- sourceText.Append(Indent).Append(Indent).AppendLine("{");
-
- sourceText
- .Append(Indent)
- .Append(Indent)
- .Append(Indent)
- .AppendLine("RegisterGeneratedDataLoader(builder);");
- sourceText.AppendLine();
-
- var operations = OperationType.No;
-
- foreach (var syntaxInfo in syntaxInfos)
- {
- switch (syntaxInfo)
- {
- case TypeInfo type:
- if ((module.Options & ModuleOptions.RegisterTypes) ==
- ModuleOptions.RegisterTypes)
- {
- sourceText.Append(Indent)
- .Append(Indent)
- .Append(Indent)
- .Append("builder.AddType();");
- }
- break;
-
- case TypeExtensionInfo extension:
- if ((module.Options & ModuleOptions.RegisterTypes) ==
- ModuleOptions.RegisterTypes)
- {
- if (extension.IsStatic)
- {
- sourceText.Append(Indent)
- .Append(Indent)
- .Append(Indent)
- .Append("builder.AddTypeExtension(typeof(global::")
- .Append(extension.Name)
- .AppendLine("));");
- }
- else
- {
- sourceText.Append(Indent)
- .Append(Indent)
- .Append(Indent)
- .Append("builder.AddTypeExtension();");
- }
-
- if (extension.Type is not OperationType.No &&
- (operations & extension.Type) != extension.Type)
- {
- operations |= extension.Type;
- }
- }
- break;
-
- case RegisterDataLoaderInfo dataLoader:
- if ((module.Options & ModuleOptions.RegisterDataLoader) ==
- ModuleOptions.RegisterDataLoader)
- {
- sourceText.Append(Indent)
- .Append(Indent)
- .Append(Indent)
- .Append("builder.AddDataLoader();");
- }
- break;
- }
- }
-
- if ((operations & OperationType.Query) == OperationType.Query)
- {
- WriteTryAddOperationType(sourceText, OperationType.Query);
- }
-
- if ((operations & OperationType.Mutation) == OperationType.Mutation)
- {
- WriteTryAddOperationType(sourceText, OperationType.Mutation);
- }
-
- if ((operations & OperationType.Subscription) == OperationType.Subscription)
- {
- WriteTryAddOperationType(sourceText, OperationType.Subscription);
- }
-
- sourceText.Append(Indent).Append(Indent).Append(Indent).AppendLine("return builder;");
- sourceText.Append(Indent).Append(Indent).AppendLine("}");
-
- sourceText.AppendLine();
-
- sourceText
- .Append(Indent)
- .Append(Indent)
- .Append("static partial void RegisterGeneratedDataLoader(")
- .AppendLine("IRequestExecutorBuilder builder);");
-
- sourceText.Append(Indent).AppendLine("}");
-
- sourceText.AppendLine("}");
-
- context.AddSource(TypeModuleFile, SourceText.From(sourceText.ToString(), Encoding.UTF8));
- StringBuilderPool.Return(sourceText);
- }
-
- private static void WriteTryAddOperationType(StringBuilder sourceText, OperationType type)
- => sourceText.Append(Indent)
- .Append(Indent)
- .Append(Indent)
- .AppendLine("builder.ConfigureSchema(")
- .Append(Indent)
- .Append(Indent)
- .Append(Indent)
- .Append(Indent)
- .AppendLine("b => b.TryAddRootType(")
- .Append(Indent)
- .Append(Indent)
- .Append(Indent)
- .Append(Indent)
- .Append(Indent)
- .AppendLine("() => new global::HotChocolate.Types.ObjectType(")
- .Append(Indent)
- .Append(Indent)
- .Append(Indent)
- .Append(Indent)
- .Append(Indent)
- .Append(Indent)
- .Append("d => d.Name(global::HotChocolate.Types.OperationTypeNames.")
- .Append(type)
- .AppendLine(")),")
- .Append(Indent)
- .Append(Indent)
- .Append(Indent)
- .Append(Indent)
- .Append(Indent)
- .Append("HotChocolate.Language.OperationType.")
- .Append(type)
- .AppendLine("));");
-}
diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Generators/ModuleSyntaxGenerator.cs b/src/HotChocolate/Core/src/Types.Analyzers/Generators/ModuleSyntaxGenerator.cs
new file mode 100644
index 00000000000..62a8ed795f2
--- /dev/null
+++ b/src/HotChocolate/Core/src/Types.Analyzers/Generators/ModuleSyntaxGenerator.cs
@@ -0,0 +1,124 @@
+using System.Text;
+using HotChocolate.Types.Analyzers.Helpers;
+using Microsoft.CodeAnalysis.Text;
+
+namespace HotChocolate.Types.Analyzers.Generators;
+
+public sealed class ModuleSyntaxGenerator : IDisposable
+{
+ private readonly string _moduleName;
+ private readonly string _ns;
+ private StringBuilder _sb;
+ private CodeWriter _writer;
+ private bool _disposed;
+
+ public ModuleSyntaxGenerator(string moduleName, string ns)
+ {
+ _moduleName = moduleName;
+ _ns = ns;
+ _sb = StringBuilderPool.Get();
+ _writer = new CodeWriter(_sb);
+ }
+
+ public void WriterHeader()
+ => _writer.WriteFileHeader();
+
+ public void WriteBeginNamespace()
+ {
+ _writer.WriteIndentedLine("namespace {0}", _ns);
+ _writer.WriteIndentedLine("{");
+ _writer.IncreaseIndent();
+ }
+
+ public void WriteEndNamespace()
+ {
+ _writer.DecreaseIndent();
+ _writer.WriteIndentedLine("}");
+ }
+
+ public void WriteBeginClass()
+ {
+ _writer.WriteIndentedLine("public static partial class {0}RequestExecutorBuilderExtensions", _moduleName);
+ _writer.WriteIndentedLine("{");
+ _writer.IncreaseIndent();
+ }
+
+ public void WriteEndClass()
+ {
+ _writer.DecreaseIndent();
+ _writer.WriteIndentedLine("}");
+ }
+
+ public void WriteBeginRegistrationMethod()
+ {
+ _writer.WriteIndentedLine(
+ "public static IRequestExecutorBuilder Add{0}(this IRequestExecutorBuilder builder)",
+ _moduleName);
+ _writer.WriteIndentedLine("{");
+ _writer.IncreaseIndent();
+ }
+
+ public void WriteEndRegistrationMethod()
+ {
+ _writer.WriteIndentedLine("return builder;");
+ _writer.DecreaseIndent();
+ _writer.WriteIndentedLine("}");
+ }
+
+ public void WriteRegisterType(string typeName)
+ => _writer.WriteIndentedLine("builder.AddType();", typeName);
+
+ public void WriteRegisterTypeExtension(string typeName, bool staticType)
+ => _writer.WriteIndentedLine(
+ staticType
+ ? "builder.AddTypeExtension(typeof(global::{0}));"
+ : "builder.AddTypeExtension();",
+ typeName);
+
+ public void WriteRegisterDataLoader(string typeName)
+ => _writer.WriteIndentedLine("builder.AddDataLoader();", typeName);
+
+ public void WriteRegisterDataLoader(string typeName, string interfaceTypeName)
+ => _writer.WriteIndentedLine("builder.AddDataLoader();", interfaceTypeName, typeName);
+
+ public void WriteTryAddOperationType(OperationType type)
+ {
+ _writer.WriteIndentedLine("builder.ConfigureSchema(");
+
+ using (_writer.IncreaseIndent())
+ {
+ _writer.WriteIndentedLine("b => b.TryAddRootType(");
+
+ using (_writer.IncreaseIndent())
+ {
+ _writer.WriteIndentedLine("() => new global::HotChocolate.Types.ObjectType(");
+
+ using (_writer.IncreaseIndent())
+ {
+ _writer.WriteIndentedLine("d => d.Name(global::HotChocolate.Types.OperationTypeNames.{0})),", type);
+ }
+
+ _writer.WriteIndentedLine("HotChocolate.Language.OperationType.{0}));", type);
+ }
+ }
+ }
+
+ public override string ToString()
+ => _sb.ToString();
+
+ public SourceText ToSourceText()
+ => SourceText.From(ToString(), Encoding.UTF8);
+
+ public void Dispose()
+ {
+ if (_disposed)
+ {
+ return;
+ }
+
+ StringBuilderPool.Return(_sb);
+ _sb = default!;
+ _writer = default!;
+ _disposed = true;
+ }
+}
\ No newline at end of file
diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Generators/OperationFieldSyntaxGenerator.cs b/src/HotChocolate/Core/src/Types.Analyzers/Generators/OperationFieldSyntaxGenerator.cs
new file mode 100644
index 00000000000..dc7f9c57a74
--- /dev/null
+++ b/src/HotChocolate/Core/src/Types.Analyzers/Generators/OperationFieldSyntaxGenerator.cs
@@ -0,0 +1,133 @@
+using System.Text;
+using HotChocolate.Types.Analyzers.Helpers;
+using HotChocolate.Types.Analyzers.Inspectors;
+using Microsoft.CodeAnalysis.Text;
+
+namespace HotChocolate.Types.Analyzers.Generators;
+
+public sealed class OperationFieldSyntaxGenerator: IDisposable
+{
+ private StringBuilder _sb;
+ private CodeWriter _writer;
+ private bool _first = true;
+ private bool _disposed;
+
+ public OperationFieldSyntaxGenerator()
+ {
+ _sb = StringBuilderPool.Get();
+ _writer = new CodeWriter(_sb);
+ }
+
+ public void WriterHeader()
+ {
+ _writer.WriteFileHeader();
+ _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 void WriteBeginClass(string typeName)
+ {
+ if (!_first)
+ {
+ _writer.WriteLine();
+ }
+ _first = false;
+
+ _writer.WriteIndentedLine("public sealed class {0}", typeName);
+
+ using (_writer.IncreaseIndent())
+ {
+ _writer.WriteIndentedLine(": global::HotChocolate.Types.ObjectTypeExtension");
+ }
+ _writer.WriteIndentedLine("{");
+ _writer.IncreaseIndent();
+ }
+
+ public void WriteEndClass()
+ {
+ _writer.DecreaseIndent();
+ _writer.WriteIndentedLine("}");
+ }
+
+ public void WriteConfigureMethod(OperationType type, IEnumerable operations)
+ {
+ _writer.WriteIndentedLine("protected override void Configure(");
+ using (_writer.IncreaseIndent())
+ {
+ _writer.WriteIndentedLine("global::HotChocolate.Types.IObjectTypeDescriptor descriptor)");
+ }
+ _writer.WriteIndentedLine("{");
+ _writer.IncreaseIndent();
+
+ _writer.WriteIndentedLine("var bindingFlags = System.Reflection.BindingFlags.Public |");
+
+ using (_writer.IncreaseIndent())
+ {
+ _writer.WriteIndentedLine("System.Reflection.BindingFlags.NonPublic |");
+ _writer.WriteIndentedLine("System.Reflection.BindingFlags.Static;");
+ }
+
+ _writer.WriteIndentedLine("descriptor.Name({0});", GetOperationConstant(type));
+
+ var typeIndex = 0;
+ foreach (var group in operations.GroupBy(t => t.TypeName))
+ {
+ _writer.WriteLine();
+
+ var typeName = $"type{++typeIndex}";
+ _writer.WriteIndentedLine("var {0} = typeof({1});", typeName, group.Key);
+
+ foreach (var operation in group)
+ {
+ _writer.WriteIndentedLine(
+ "descriptor.Field({0}.GetMember(\"{1}\", bindingFlags)[0]);",
+ typeName,
+ operation.MethodName);
+ }
+ }
+
+ _writer.DecreaseIndent();
+ _writer.WriteIndentedLine("}");
+ }
+
+ private static string GetOperationConstant(OperationType type)
+ => type switch
+ {
+ OperationType.Query => "global::HotChocolate.Types.OperationTypeNames.Query",
+ OperationType.Mutation => "global::HotChocolate.Types.OperationTypeNames.Mutation",
+ OperationType.Subscription => "global::HotChocolate.Types.OperationTypeNames.Subscription",
+ _ => throw new InvalidOperationException()
+ };
+
+ public override string ToString()
+ => _sb.ToString();
+
+ public SourceText ToSourceText()
+ => SourceText.From(ToString(), Encoding.UTF8);
+
+ public void Dispose()
+ {
+ if (_disposed)
+ {
+ return;
+ }
+
+ StringBuilderPool.Return(_sb);
+ _sb = default!;
+ _writer = default!;
+ _disposed = true;
+ }
+}
\ No newline at end of file
diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Helpers/CodeWriter.cs b/src/HotChocolate/Core/src/Types.Analyzers/Helpers/CodeWriter.cs
new file mode 100644
index 00000000000..3bef6695746
--- /dev/null
+++ b/src/HotChocolate/Core/src/Types.Analyzers/Helpers/CodeWriter.cs
@@ -0,0 +1,156 @@
+using System.Text;
+
+namespace HotChocolate.Types.Analyzers.Helpers;
+
+public class CodeWriter : TextWriter
+{
+ private readonly TextWriter _writer;
+ private readonly bool _disposeWriter;
+ private bool _disposed;
+ private int _indent;
+
+ public CodeWriter(TextWriter writer)
+ {
+ _writer = writer;
+ _disposeWriter = false;
+ }
+
+ public CodeWriter(StringBuilder text)
+ {
+ _writer = new StringWriter(text);
+ _disposeWriter = true;
+ }
+
+ public override Encoding Encoding { get; } = Encoding.UTF8;
+
+ public static string Indent { get; } = new(' ', 4);
+
+ public override void Write(char value) =>
+ _writer.Write(value);
+
+ public void WriteStringValue(string value)
+ {
+ Write('"');
+ Write(value);
+ Write('"');
+ }
+
+ public void WriteIndent()
+ {
+ if (_indent > 0)
+ {
+ var spaces = _indent * 4;
+ for (var i = 0; i < spaces; i++)
+ {
+ Write(' ');
+ }
+ }
+ }
+
+ public string GetIndentString()
+ {
+ if (_indent > 0)
+ {
+ return new string(' ', _indent * 4);
+ }
+ return string.Empty;
+ }
+
+ public void WriteIndentedLine(string format, params object?[] args)
+ {
+ WriteIndent();
+
+ if (args.Length == 0)
+ {
+ Write(format);
+ }
+ else
+ {
+ Write(format, args);
+ }
+
+ WriteLine();
+ }
+
+ public void WriteIndented(string format, params object?[] args)
+ {
+ WriteIndent();
+
+ if (args.Length == 0)
+ {
+ Write(format);
+ }
+ else
+ {
+ Write(format, args);
+ }
+ }
+
+ public void WriteSpace() => Write(' ');
+
+ public IDisposable IncreaseIndent()
+ {
+ _indent++;
+ return new Block(DecreaseIndent);
+ }
+
+ public void DecreaseIndent()
+ {
+ if (_indent > 0)
+ {
+ _indent--;
+ }
+ }
+
+ public IDisposable WriteBraces()
+ {
+ WriteLeftBrace();
+ WriteLine();
+
+#pragma warning disable CA2000
+ var indent = IncreaseIndent();
+#pragma warning restore CA2000
+
+ return new Block(() =>
+ {
+ WriteLine();
+ indent.Dispose();
+ WriteIndent();
+ WriteRightBrace();
+ });
+ }
+
+ public void WriteLeftBrace() => Write('{');
+
+ public void WriteRightBrace() => Write('}');
+
+ public override void Flush()
+ {
+ base.Flush();
+ _writer.Flush();
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ if (!_disposed && _disposeWriter)
+ {
+ if (disposing)
+ {
+ _writer.Dispose();
+ }
+ _disposed = true;
+ }
+ }
+
+ private sealed class Block : IDisposable
+ {
+ private readonly Action _decrease;
+
+ public Block(Action close)
+ {
+ _decrease = close;
+ }
+
+ public void Dispose() => _decrease();
+ }
+}
\ No newline at end of file
diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Helpers/CodeWriterExtensions.cs b/src/HotChocolate/Core/src/Types.Analyzers/Helpers/CodeWriterExtensions.cs
new file mode 100644
index 00000000000..43ddf5be758
--- /dev/null
+++ b/src/HotChocolate/Core/src/Types.Analyzers/Helpers/CodeWriterExtensions.cs
@@ -0,0 +1,45 @@
+namespace HotChocolate.Types.Analyzers.Helpers;
+
+public static class CodeWriterExtensions
+{
+ public static void WriteGeneratedAttribute(this CodeWriter writer)
+ {
+ if (writer is null)
+ {
+ throw new ArgumentNullException(nameof(writer));
+ }
+
+#if DEBUG
+ writer.WriteIndentedLine(
+ "[global::System.CodeDom.Compiler.GeneratedCode(" +
+ "\"HotChocolate\", \"11.0.0\")]");
+#else
+ var version = typeof(CodeWriter).Assembly.GetName().Version!.ToString();
+ writer.WriteIndentedLine(
+ "[global::System.CodeDom.Compiler.GeneratedCode(" +
+ $"\"HotChocolate\", \"{version}\")]");
+#endif
+ }
+
+ public static void WriteFileHeader(this CodeWriter writer)
+ {
+ if (writer is null)
+ {
+ throw new ArgumentNullException(nameof(writer));
+ }
+
+ writer.WriteIndentedLine("// ");
+ writer.WriteLine();
+ writer.WriteIndentedLine("#nullable enable");
+ writer.WriteLine();
+ writer.WriteIndentedLine("using System;");
+ writer.WriteIndentedLine("using HotChocolate.Execution.Configuration;");
+ }
+
+ public static CodeWriter WriteComment(this CodeWriter writer, string comment)
+ {
+ writer.Write("// ");
+ writer.WriteLine(comment);
+ return writer;
+ }
+}
\ No newline at end of file
diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Helpers/GeneratorUtils.cs b/src/HotChocolate/Core/src/Types.Analyzers/Helpers/GeneratorUtils.cs
new file mode 100644
index 00000000000..8c4ee3d95ed
--- /dev/null
+++ b/src/HotChocolate/Core/src/Types.Analyzers/Helpers/GeneratorUtils.cs
@@ -0,0 +1,44 @@
+using System.Collections.Immutable;
+using HotChocolate.Types.Analyzers.Inspectors;
+
+namespace HotChocolate.Types.Analyzers.Helpers;
+
+internal static class GeneratorUtils
+{
+ public static ModuleInfo GetModuleInfo(
+ this ImmutableArray syntaxInfos,
+ string? assemblyName,
+ out bool defaultModule)
+ {
+ foreach (var syntaxInfo in syntaxInfos)
+ {
+ if (syntaxInfo is ModuleInfo module)
+ {
+ defaultModule = false;
+ return module;
+ }
+ }
+
+ defaultModule = true;
+ return new ModuleInfo(CreateModuleName(assemblyName), ModuleOptions.Default);
+ }
+
+ public static DataLoaderDefaultsInfo GetDataLoaderDefaults(
+ this ImmutableArray syntaxInfos)
+ {
+ foreach (var syntaxInfo in syntaxInfos)
+ {
+ if (syntaxInfo is DataLoaderDefaultsInfo defaults)
+ {
+ return defaults;
+ }
+ }
+
+ return new DataLoaderDefaultsInfo(null, null, true, true);
+ }
+
+ private static string CreateModuleName(string? assemblyName)
+ => assemblyName is null
+ ? "AssemblyTypes"
+ : assemblyName.Split('.').Last() + "Types";
+}
\ No newline at end of file
diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/DataLoaderInspector.cs b/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/DataLoaderInspector.cs
index 40fcead47a4..4e99fd5733d 100644
--- a/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/DataLoaderInspector.cs
+++ b/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/DataLoaderInspector.cs
@@ -45,4 +45,4 @@ public bool TryHandle(
syntaxInfo = null;
return false;
}
-}
+}
\ No newline at end of file
diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/OperationInfo.cs b/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/OperationInfo.cs
new file mode 100644
index 00000000000..d2616268ea3
--- /dev/null
+++ b/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/OperationInfo.cs
@@ -0,0 +1,58 @@
+namespace HotChocolate.Types.Analyzers.Inspectors;
+
+public sealed class OperationInfo(OperationType type, string typeName, string methodName) : ISyntaxInfo
+{
+ public OperationType Type { get; } = type;
+
+ public string TypeName { get; } = typeName;
+
+ public string MethodName { get; } = methodName;
+
+ public bool Equals(OperationInfo? other)
+ {
+ if (ReferenceEquals(null, other))
+ {
+ return false;
+ }
+
+ if (ReferenceEquals(this, other))
+ {
+ return true;
+ }
+
+ return Type.Equals(other.Type) &&
+ TypeName.Equals(other.TypeName, StringComparison.Ordinal) &&
+ MethodName.Equals(other.MethodName, StringComparison.Ordinal);
+ }
+
+ public bool Equals(ISyntaxInfo other)
+ {
+ if (ReferenceEquals(null, other))
+ {
+ return false;
+ }
+
+ if (ReferenceEquals(this, other))
+ {
+ return true;
+ }
+
+ return other is OperationInfo info && Equals(info);
+ }
+
+ public override bool Equals(object? obj)
+ => ReferenceEquals(this, obj)
+ || obj is DataLoaderInfo other && Equals(other);
+
+ public override int GetHashCode()
+ {
+ unchecked
+ {
+ var hashCode = 5;
+ hashCode = (hashCode * 397) ^ Type.GetHashCode();
+ hashCode = (hashCode * 397) ^ TypeName.GetHashCode();
+ hashCode = (hashCode * 397) ^ MethodName.GetHashCode();
+ return hashCode;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/OperationInspector.cs b/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/OperationInspector.cs
new file mode 100644
index 00000000000..90ffeff82fd
--- /dev/null
+++ b/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/OperationInspector.cs
@@ -0,0 +1,78 @@
+using System.Diagnostics.CodeAnalysis;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+
+namespace HotChocolate.Types.Analyzers.Inspectors;
+
+public sealed class OperationInspector : ISyntaxInspector
+{
+ public bool TryHandle(
+ GeneratorSyntaxContext context,
+ [NotNullWhen(true)] out ISyntaxInfo? syntaxInfo)
+ {
+ if (context.Node is MethodDeclarationSyntax { AttributeLists.Count: > 0, } methodSyntax)
+ {
+ foreach (var attributeListSyntax in methodSyntax.AttributeLists)
+ {
+ foreach (var attributeSyntax in attributeListSyntax.Attributes)
+ {
+ var symbol = context.SemanticModel.GetSymbolInfo(attributeSyntax).Symbol;
+
+ if (symbol is not IMethodSymbol attributeSymbol)
+ {
+ continue;
+ }
+
+ var attributeContainingTypeSymbol = attributeSymbol.ContainingType;
+ var fullName = attributeContainingTypeSymbol.ToDisplayString();
+ var operationType = ParseOperationType(fullName);
+
+ if(operationType == OperationType.No)
+ {
+ continue;
+ }
+
+ if (context.SemanticModel.GetDeclaredSymbol(methodSyntax) is not { } methodSymbol)
+ {
+ continue;
+ }
+
+ if (!methodSymbol.IsStatic)
+ {
+ continue;
+ }
+
+ syntaxInfo = new OperationInfo(
+ operationType,
+ methodSymbol.ContainingType.ToDisplayString(),
+ methodSymbol.Name);
+ return true;
+ }
+ }
+ }
+
+ syntaxInfo = null;
+ return false;
+ }
+
+ private OperationType ParseOperationType(string attributeName)
+ {
+ if (attributeName.Equals(WellKnownAttributes.QueryAttribute, StringComparison.Ordinal))
+ {
+ return OperationType.Query;
+ }
+
+ if (attributeName.Equals(WellKnownAttributes.MutationAttribute, StringComparison.Ordinal))
+ {
+ return OperationType.Mutation;
+ }
+
+ if (attributeName.Equals(WellKnownAttributes.SubscriptionAttribute, StringComparison.Ordinal))
+ {
+ return OperationType.Subscription;
+ }
+
+ return OperationType.No;
+ }
+}
\ No newline at end of file
diff --git a/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/OperationRegistrationInfo.cs b/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/OperationRegistrationInfo.cs
new file mode 100644
index 00000000000..2f7671c4c76
--- /dev/null
+++ b/src/HotChocolate/Core/src/Types.Analyzers/Inspectors/OperationRegistrationInfo.cs
@@ -0,0 +1,52 @@
+namespace HotChocolate.Types.Analyzers.Inspectors;
+
+public sealed class OperationRegistrationInfo(OperationType type, string typeName) : ISyntaxInfo
+{
+ public OperationType Type { get; } = type;
+
+ public string TypeName { get; } = typeName;
+
+ public bool Equals(OperationRegistrationInfo? other)
+ {
+ if (ReferenceEquals(null, other))
+ {
+ return false;
+ }
+
+ if (ReferenceEquals(this, other))
+ {
+ return true;
+ }
+
+ return TypeName.Equals(other.TypeName, StringComparison.Ordinal);
+ }
+
+ public bool Equals(ISyntaxInfo other)
+ {
+ if (ReferenceEquals(null, other))
+ {
+ return false;
+ }
+
+ if (ReferenceEquals(this, other))
+ {
+ return true;
+ }
+
+ return other is OperationInfo info && Equals(info);
+ }
+
+ public override bool Equals(object? obj)
+ => ReferenceEquals(this, obj)
+ || obj is DataLoaderInfo other && Equals(other);
+
+ public override int GetHashCode()
+ {
+ unchecked
+ {
+ var hashCode = 5;
+ hashCode = (hashCode * 397) ^ TypeName.GetHashCode();
+ return hashCode;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/HotChocolate/Core/src/Types.Analyzers/TypeModuleGenerator.cs b/src/HotChocolate/Core/src/Types.Analyzers/TypeModuleGenerator.cs
index d0e92549d73..821bafedb7e 100644
--- a/src/HotChocolate/Core/src/Types.Analyzers/TypeModuleGenerator.cs
+++ b/src/HotChocolate/Core/src/Types.Analyzers/TypeModuleGenerator.cs
@@ -1,9 +1,13 @@
-using System.Buffers;
-using System.Collections.Immutable;
+using System.Collections.Immutable;
using HotChocolate.Types.Analyzers.Generators;
+using HotChocolate.Types.Analyzers.Helpers;
using HotChocolate.Types.Analyzers.Inspectors;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
+using static System.StringComparison;
+using static HotChocolate.Types.Analyzers.WellKnownFileNames;
+using static HotChocolate.Types.Analyzers.WellKnownTypes;
+using TypeInfo = HotChocolate.Types.Analyzers.Inspectors.TypeInfo;
namespace HotChocolate.Types.Analyzers;
@@ -17,18 +21,11 @@ public class TypeModuleGenerator : IIncrementalGenerator
new ModuleInspector(),
new DataLoaderInspector(),
new DataLoaderDefaultsInspector(),
- ];
-
- private static readonly ISyntaxGenerator[] _generators =
- [
- new ModuleGenerator(),
- new DataLoaderGenerator(),
+ new OperationInspector(),
];
public void Initialize(IncrementalGeneratorInitializationContext context)
{
- context.RegisterPostInitializationOutput(c => PostInitialization(c));
-
var modulesAndTypes =
context.SyntaxProvider
.CreateSyntaxProvider(
@@ -44,14 +41,6 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
static (context, source) => Execute(context, source.Left, source.Right));
}
- private static void PostInitialization(IncrementalGeneratorPostInitializationContext context)
- {
- foreach (var syntaxGenerator in _generators)
- {
- syntaxGenerator.Initialize(context);
- }
- }
-
private static bool IsRelevant(SyntaxNode node)
=> IsTypeWithAttribute(node) ||
IsClassWithBaseClass(node) ||
@@ -95,37 +84,414 @@ private static void Execute(
return;
}
- var buffer = ArrayPool.Shared.Rent(syntaxInfos.Length * 2);
+ var module = syntaxInfos.GetModuleInfo(compilation.AssemblyName, out var defaultModule);
+ var dataLoaderDefaults = syntaxInfos.GetDataLoaderDefaults();
+
+ // if there is only the module info we do not need to generate a module.
+ if (!defaultModule && syntaxInfos.Length == 1)
+ {
+ return;
+ }
+
+ var syntaxInfoList = syntaxInfos.ToList();
+ WriteOperationTypes(context, syntaxInfoList, module);
+ WriteDataLoader(context, syntaxInfoList, dataLoaderDefaults);
+ WriteConfiguration(context, syntaxInfoList, module);
+ }
+
+ private static void WriteConfiguration(
+ SourceProductionContext context,
+ List syntaxInfos,
+ ModuleInfo module)
+ {
+ using var generator = new ModuleSyntaxGenerator(module.ModuleName, "Microsoft.Extensions.DependencyInjection");
+
+ generator.WriterHeader();
+ generator.WriteBeginNamespace();
+ generator.WriteBeginClass();
+ generator.WriteBeginRegistrationMethod();
+
+ var operations = OperationType.No;
+
+ foreach (var syntaxInfo in syntaxInfos)
+ {
+ switch (syntaxInfo)
+ {
+ case TypeInfo type:
+ if ((module.Options & ModuleOptions.RegisterTypes) ==
+ ModuleOptions.RegisterTypes)
+ {
+ generator.WriteRegisterType(type.Name);
+ }
+ break;
+
+ case TypeExtensionInfo extension:
+ if ((module.Options & ModuleOptions.RegisterTypes) ==
+ ModuleOptions.RegisterTypes)
+ {
+ generator.WriteRegisterTypeExtension(extension.Name, extension.IsStatic);
+
+ if (extension.Type is not OperationType.No &&
+ (operations & extension.Type) != extension.Type)
+ {
+ operations |= extension.Type;
+ }
+ }
+ break;
+
+ case RegisterDataLoaderInfo dataLoader:
+ if ((module.Options & ModuleOptions.RegisterDataLoader) ==
+ ModuleOptions.RegisterDataLoader)
+ {
+ generator.WriteRegisterDataLoader(dataLoader.Name);
+ }
+ break;
+
+ case DataLoaderInfo dataLoader:
+ if ((module.Options & ModuleOptions.RegisterDataLoader) ==
+ ModuleOptions.RegisterDataLoader)
+ {
+ var typeName = $"{dataLoader.Namespace}.{dataLoader.Name}";
+ var interfaceTypeName = $"{dataLoader.Namespace}.{dataLoader.InterfaceName}";
+
+ generator.WriteRegisterDataLoader(typeName, interfaceTypeName);
+ }
+ break;
+
+ case OperationRegistrationInfo operation:
+ if ((module.Options & ModuleOptions.RegisterTypes) ==
+ ModuleOptions.RegisterTypes)
+ {
+ generator.WriteRegisterTypeExtension(operation.TypeName, false);
+
+ if (operation.Type is not OperationType.No &&
+ (operations & operation.Type) != operation.Type)
+ {
+ operations |= operation.Type;
+ }
+ }
+ break;
+ }
+ }
- // prepare context
- for (var i = syntaxInfos.Length - 1; i >= 0; i--)
+ if ((operations & OperationType.Query) == OperationType.Query)
{
- buffer[i] = syntaxInfos[i];
+ generator.WriteTryAddOperationType(OperationType.Query);
}
- var nodes = buffer.AsSpan().Slice(0, syntaxInfos.Length);
- var batch = buffer.AsSpan().Slice(syntaxInfos.Length, syntaxInfos.Length);
+ if ((operations & OperationType.Mutation) == OperationType.Mutation)
+ {
+ generator.WriteTryAddOperationType(OperationType.Mutation);
+ }
- foreach (var generator in _generators)
+ if ((operations & OperationType.Subscription) == OperationType.Subscription)
{
- var next = 0;
+ generator.WriteTryAddOperationType(OperationType.Subscription);
+ }
+
+ generator.WriteEndRegistrationMethod();
+ generator.WriteEndClass();
+ generator.WriteEndNamespace();
- // gather infos for current generator
- foreach (var node in nodes)
+ context.AddSource(TypeModuleFile, generator.ToSourceText());
+ }
+
+ private static void WriteDataLoader(
+ SourceProductionContext context,
+ List syntaxInfos,
+ DataLoaderDefaultsInfo defaults)
+ {
+ var dataLoaders = new List();
+
+ foreach (var syntaxInfo in syntaxInfos)
+ {
+ if (syntaxInfo is not DataLoaderInfo dataLoader)
+ {
+ continue;
+ }
+
+ if (dataLoader.MethodSymbol.Parameters.Length == 0)
+ {
+ context.ReportDiagnostic(
+ Diagnostic.Create(
+ Errors.KeyParameterMissing,
+ Location.Create(
+ dataLoader.MethodSyntax.SyntaxTree,
+ dataLoader.MethodSyntax.ParameterList.Span)));
+ continue;
+ }
+
+ if (dataLoader.MethodSymbol.DeclaredAccessibility is not Accessibility.Public
+ and not Accessibility.Internal and not Accessibility.ProtectedAndInternal)
+ {
+ context.ReportDiagnostic(
+ Diagnostic.Create(
+ Errors.MethodAccessModifierInvalid,
+ Location.Create(
+ dataLoader.MethodSyntax.SyntaxTree,
+ dataLoader.MethodSyntax.Modifiers.Span)));
+ continue;
+ }
+
+ dataLoaders.Add(dataLoader);
+ }
+
+ var generator = new DataLoaderSyntaxGenerator();
+ generator.WriterHeader();
+
+ foreach (var group in dataLoaders.GroupBy(t => t.Namespace))
+ {
+ generator.WriteBeginNamespace(group.Key);
+
+ foreach (var dataLoader in group)
{
- if (generator.Consume(node))
+ var keyArg = dataLoader.MethodSymbol.Parameters[0];
+ var keyType = keyArg.Type;
+ var cancellationTokenIndex = -1;
+ var serviceMap = new Dictionary();
+
+ if (IsKeysArgument(keyType))
{
- batch[next++] = node;
+ keyType = ExtractKeyType(keyType);
}
+
+ InspectDataLoaderParameters(
+ dataLoader,
+ ref cancellationTokenIndex,
+ serviceMap);
+
+ DataLoaderKind kind;
+
+ if (IsReturnTypeDictionary(dataLoader.MethodSymbol.ReturnType, keyType))
+ {
+ kind = DataLoaderKind.Batch;
+ }
+ else if (IsReturnTypeLookup(dataLoader.MethodSymbol.ReturnType, keyType))
+ {
+ kind = DataLoaderKind.Group;
+ }
+ else
+ {
+ keyType = keyArg.Type;
+ kind = DataLoaderKind.Cache;
+ }
+
+ var valueType = ExtractValueType(dataLoader.MethodSymbol.ReturnType, kind);
+
+ GenerateDataLoader(
+ generator,
+ dataLoader,
+ defaults,
+ kind,
+ keyType,
+ valueType,
+ dataLoader.MethodSymbol.Parameters.Length,
+ cancellationTokenIndex,
+ serviceMap);
}
+
+ generator.WriteEndNamespace();
+ }
+
+ context.AddSource(DataLoaderFile, generator.ToSourceText());
+ }
+
+ private static void WriteOperationTypes(
+ SourceProductionContext context,
+ List syntaxInfos,
+ ModuleInfo module)
+ {
+ var operations = new List();
- // generate
- if (next > 0)
+ foreach (var syntaxInfo in syntaxInfos)
+ {
+ if (syntaxInfo is OperationInfo operation)
{
- generator.Generate(context, compilation, batch.Slice(0, next));
+ operations.Add(operation);
}
}
- ArrayPool.Shared.Return(buffer);
+ if (operations.Count == 0)
+ {
+ return;
+ }
+
+ var generator = new OperationFieldSyntaxGenerator();
+ generator.WriterHeader();
+ generator.WriteBeginNamespace("Microsoft.Extensions.DependencyInjection");
+
+ foreach (var group in operations.GroupBy(t => t.Type))
+ {
+ var typeName = $"{module.ModuleName}{group.Key}Type";
+
+ generator.WriteBeginClass(typeName);
+ generator.WriteConfigureMethod(group.Key, group);
+ generator.WriteEndClass();
+
+ syntaxInfos.Add(new OperationRegistrationInfo(
+ group.Key,
+ $"Microsoft.Extensions.DependencyInjection.{typeName}"));
+ }
+
+ generator.WriteEndNamespace();
+
+ context.AddSource(RootTypesFile, generator.ToSourceText());
+ }
+
+ private static void GenerateDataLoader(
+ DataLoaderSyntaxGenerator generator,
+ DataLoaderInfo dataLoader,
+ DataLoaderDefaultsInfo defaults,
+ DataLoaderKind kind,
+ ITypeSymbol keyType,
+ ITypeSymbol valueType,
+ int parameterCount,
+ int cancelIndex,
+ Dictionary services)
+ {
+ var isScoped = dataLoader.IsScoped ?? defaults.Scoped ?? false;
+ var isPublic = dataLoader.IsPublic ?? defaults.IsPublic ?? true;
+ var isInterfacePublic = dataLoader.IsInterfacePublic ?? defaults.IsInterfacePublic ?? true;
+
+ generator.WriteDataLoaderInterface(dataLoader.InterfaceName, isInterfacePublic, kind, keyType, valueType);
+
+ generator.WriteBeginDataLoaderClass(
+ dataLoader.Name,
+ dataLoader.InterfaceName,
+ isPublic,
+ kind,
+ keyType,
+ valueType);
+ generator.WriteDataLoaderConstructor(dataLoader.Name, kind);
+ generator.WriteDataLoaderLoadMethod(
+ dataLoader.ContainingType,
+ dataLoader.MethodName,
+ isScoped,
+ kind,
+ keyType,
+ valueType,
+ services,
+ parameterCount,
+ cancelIndex);
+ generator.WriteEndDataLoaderClass();
+ }
+
+ private static void InspectDataLoaderParameters(
+ DataLoaderInfo dataLoader,
+ ref int cancellationTokenIndex,
+ Dictionary serviceMap)
+ {
+ for (var i = 1; i < dataLoader.MethodSymbol.Parameters.Length; i++)
+ {
+ var argument = dataLoader.MethodSymbol.Parameters[i];
+ var argumentType = argument.Type.ToFullyQualified();
+
+ if (IsCancellationToken(argumentType))
+ {
+ if (cancellationTokenIndex != -1)
+ {
+ // report error
+ return;
+ }
+
+ cancellationTokenIndex = i;
+ }
+ else
+ {
+ serviceMap[i] = argumentType;
+ }
+ }
+ }
+
+ private static bool IsKeysArgument(ITypeSymbol type)
+ => type is INamedTypeSymbol { IsGenericType: true, TypeArguments.Length: 1, } nt &&
+ ReadOnlyList.Equals(ToTypeNameNoGenerics(nt), Ordinal);
+
+ private static ITypeSymbol ExtractKeyType(ITypeSymbol type)
+ {
+ if (type is INamedTypeSymbol { IsGenericType: true, TypeArguments.Length: 1, } namedType &&
+ ReadOnlyList.Equals(ToTypeNameNoGenerics(namedType), Ordinal))
+ {
+ return namedType.TypeArguments[0];
+ }
+
+ throw new InvalidOperationException();
+ }
+
+ private static bool IsCancellationToken(string typeName)
+ => string.Equals(typeName, WellKnownTypes.CancellationToken) ||
+ string.Equals(typeName, GlobalCancellationToken);
+
+ private static bool IsReturnTypeDictionary(ITypeSymbol returnType, ITypeSymbol keyType)
+ {
+ if (returnType is INamedTypeSymbol { TypeArguments.Length: 1, } namedType)
+ {
+ var resultType = namedType.TypeArguments[0];
+
+ if (IsReadOnlyDictionary(resultType) &&
+ resultType is INamedTypeSymbol { TypeArguments.Length: 2, } dictionaryType &&
+ dictionaryType.TypeArguments[0].Equals(keyType, SymbolEqualityComparer.Default))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private static bool IsReturnTypeLookup(ITypeSymbol returnType, ITypeSymbol keyType)
+ {
+ if (returnType is INamedTypeSymbol { TypeArguments.Length: 1, } namedType)
+ {
+ var resultType = namedType.TypeArguments[0];
+
+ if (ToTypeNameNoGenerics(resultType).Equals(Lookup, Ordinal) &&
+ resultType is INamedTypeSymbol { TypeArguments.Length: 2, } dictionaryType &&
+ dictionaryType.TypeArguments[0].Equals(keyType, SymbolEqualityComparer.Default))
+ {
+ return true;
+ }
+ }
+ return false;
}
-}
+
+ private static bool IsReadOnlyDictionary(ITypeSymbol type)
+ {
+ if (!ToTypeNameNoGenerics(type).Equals(ReadOnlyDictionary, Ordinal))
+ {
+ foreach (var interfaceSymbol in type.Interfaces)
+ {
+ if (ToTypeNameNoGenerics(interfaceSymbol).Equals(ReadOnlyDictionary, Ordinal))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ return true;
+ }
+
+ private static ITypeSymbol ExtractValueType(ITypeSymbol returnType, DataLoaderKind kind)
+ {
+ if (returnType is INamedTypeSymbol { TypeArguments.Length: 1, } namedType)
+ {
+ if (kind is DataLoaderKind.Batch or DataLoaderKind.Group &&
+ namedType.TypeArguments[0] is INamedTypeSymbol { TypeArguments.Length: 2, } dict)
+ {
+ return dict.TypeArguments[1];
+ }
+
+ if (kind is DataLoaderKind.Cache)
+ {
+ return namedType.TypeArguments[0];
+ }
+ }
+
+ throw new InvalidOperationException();
+ }
+
+ private static string ToTypeNameNoGenerics(ITypeSymbol typeSymbol)
+ => $"{typeSymbol.ContainingNamespace}.{typeSymbol.Name}";
+}
\ No newline at end of file
diff --git a/src/HotChocolate/Core/src/Types.Analyzers/WellKnownAttributes.cs b/src/HotChocolate/Core/src/Types.Analyzers/WellKnownAttributes.cs
index bf72578fe9c..33d8b89a118 100644
--- a/src/HotChocolate/Core/src/Types.Analyzers/WellKnownAttributes.cs
+++ b/src/HotChocolate/Core/src/Types.Analyzers/WellKnownAttributes.cs
@@ -12,6 +12,9 @@ public static class WellKnownAttributes
public const string MutationTypeAttribute = "HotChocolate.Types.MutationTypeAttribute";
public const string SubscriptionTypeAttribute = "HotChocolate.Types.SubscriptionTypeAttribute";
public const string DataLoaderAttribute = "HotChocolate.DataLoaderAttribute";
+ public const string QueryAttribute = "HotChocolate.QueryFieldAttribute";
+ public const string MutationAttribute = "HotChocolate.MutationFieldAttribute";
+ public const string SubscriptionAttribute = "HotChocolate.SubscriptionFieldAttribute";
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 d782696a6aa..38fd2bb9d12 100644
--- a/src/HotChocolate/Core/src/Types.Analyzers/WellKnownFileNames.cs
+++ b/src/HotChocolate/Core/src/Types.Analyzers/WellKnownFileNames.cs
@@ -5,4 +5,5 @@ public static class WellKnownFileNames
public const string TypeModuleFile = "HotChocolateTypeModule.g.cs";
public const string DataLoaderFile = "HotChocolateDataLoader.g.cs";
public const string AttributesFile = "HotChocolateAttributes.g.cs";
+ public const string RootTypesFile = "HotChocolateRootTypes.g.cs";
}
diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/AnnotationBasedSchemaTests.cs b/src/HotChocolate/Core/test/Types.Analyzers.Tests/AnnotationBasedSchemaTests.cs
index 0bd477599c2..eb26a042d10 100644
--- a/src/HotChocolate/Core/test/Types.Analyzers.Tests/AnnotationBasedSchemaTests.cs
+++ b/src/HotChocolate/Core/test/Types.Analyzers.Tests/AnnotationBasedSchemaTests.cs
@@ -18,4 +18,16 @@ public async Task SchemaSnapshot()
schema.MatchSnapshot();
}
+
+ [Fact]
+ public async Task ExecuteRootField()
+ {
+ var result =
+ await new ServiceCollection()
+ .AddGraphQL()
+ .AddCustomModule()
+ .ExecuteRequestAsync("{ foo }");
+
+ result.MatchSnapshot();
+ }
}
diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/Log.txt b/src/HotChocolate/Core/test/Types.Analyzers.Tests/Log.txt
deleted file mode 100644
index f36e1340770..00000000000
--- a/src/HotChocolate/Core/test/Types.Analyzers.Tests/Log.txt
+++ /dev/null
@@ -1,6 +0,0 @@
-scoped: global::HotChocolate.Types.GenericService>
-non scoped: global::HotChocolate.Types.SomeService
-scoped: global::HotChocolate.Types.SomeService
-scoped: global::HotChocolate.Types.SomeService
-scoped: global::HotChocolate.Types.SomeService
-scoped: global::HotChocolate.Types.SomeService
diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/RootTypeTests.cs b/src/HotChocolate/Core/test/Types.Analyzers.Tests/RootTypeTests.cs
new file mode 100644
index 00000000000..cdb0d452d00
--- /dev/null
+++ b/src/HotChocolate/Core/test/Types.Analyzers.Tests/RootTypeTests.cs
@@ -0,0 +1,10 @@
+namespace HotChocolate.Types;
+
+public static class RootTypeTests
+{
+ [QueryField]
+ public static string Foo() => "foo";
+
+ [MutationField]
+ public static string Bar() => "bar";
+}
\ No newline at end of file
diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/SchemaTests.ExecuteRootField.snap b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/SchemaTests.ExecuteRootField.snap
new file mode 100644
index 00000000000..9ba5fdcb2d5
--- /dev/null
+++ b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/SchemaTests.ExecuteRootField.snap
@@ -0,0 +1,5 @@
+{
+ "data": {
+ "foo": "foo"
+ }
+}
\ No newline at end of file
diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/SchemaTests.SchemaSnapshot.graphql b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/SchemaTests.SchemaSnapshot.graphql
index 7de6b9fc193..ee37cdb2295 100644
--- a/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/SchemaTests.SchemaSnapshot.graphql
+++ b/src/HotChocolate/Core/test/Types.Analyzers.Tests/__snapshots__/SchemaTests.SchemaSnapshot.graphql
@@ -9,6 +9,7 @@ interface Entity {
}
type Mutation {
+ bar: String!
doSomething: String!
}
@@ -19,6 +20,7 @@ type Person implements Entity {
}
type Query {
+ foo: String!
person: Entity
enum: CustomEnum
book: SomeBook!
diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/ss.cs b/src/HotChocolate/Core/test/Types.Analyzers.Tests/ss.cs
deleted file mode 100644
index e69de29bb2d..00000000000