diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
new file mode 100644
index 0000000..4bb7923
--- /dev/null
+++ b/.github/workflows/build.yml
@@ -0,0 +1,28 @@
+name: Build and Test
+
+on:
+ push:
+ branches: [ main ]
+ pull_request:
+ branches: [ main ]
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v3
+
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v3
+ with:
+ dotnet-version: 8.0.x
+
+ - name: Restore dependencies
+ run: dotnet restore
+
+ - name: Build
+ run: dotnet build --no-restore --configuration Release
+
+ - name: Test
+ run: dotnet test --no-build --configuration Release --verbosity normal
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
new file mode 100644
index 0000000..cd1e184
--- /dev/null
+++ b/.github/workflows/publish.yml
@@ -0,0 +1,32 @@
+name: Publish NuGet Package
+
+on:
+ release:
+ types: [published]
+
+jobs:
+ build-and-publish:
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v3
+
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v3
+ with:
+ dotnet-version: 6.0.x
+
+ - name: Restore dependencies
+ run: dotnet restore
+
+ - name: Build
+ run: dotnet build --no-restore --configuration Release
+
+ - name: Test
+ run: dotnet test --no-build --configuration Release --verbosity normal
+
+ - name: Pack
+ run: dotnet pack GraphQLSourceGen/GraphQLSourceGen.csproj --no-build --configuration Release -o ./nupkgs
+
+ - name: Push to NuGet
+ run: dotnet nuget push ./nupkgs/*.nupkg --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..e69de29
diff --git a/GraphQLSourceGen.Samples/GraphQLSourceGen.Samples.csproj b/GraphQLSourceGen.Samples/GraphQLSourceGen.Samples.csproj
new file mode 100644
index 0000000..4fd4adb
--- /dev/null
+++ b/GraphQLSourceGen.Samples/GraphQLSourceGen.Samples.csproj
@@ -0,0 +1,26 @@
+
+
+
+ Exe
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+ GraphQL.Generated
+ true
+
+
+
\ No newline at end of file
diff --git a/GraphQLSourceGen.Samples/Program.cs b/GraphQLSourceGen.Samples/Program.cs
new file mode 100644
index 0000000..5a1f4fb
--- /dev/null
+++ b/GraphQLSourceGen.Samples/Program.cs
@@ -0,0 +1,76 @@
+using System;
+using System.Collections.Generic;
+
+namespace GraphQLSourceGen.Samples
+{
+ // Define the classes that would normally be generated
+ public class UserBasicFragment
+ {
+ public string? Id { get; set; }
+ public string? Name { get; set; }
+ public string? Email { get; set; }
+ public bool? IsActive { get; set; }
+ }
+
+ public class PostWithStatsFragment
+ {
+ public string? Id { get; set; }
+ public string? Title { get; set; }
+ public int? ViewCount { get; set; }
+ public double? Rating { get; set; }
+ public bool IsPublished { get; set; }
+ public DateTime? PublishedAt { get; set; }
+ public List? Tags { get; set; }
+ public List Categories { get; set; } = new List();
+ }
+
+ class Program
+ {
+ static void Main(string[] args)
+ {
+ Console.WriteLine("GraphQL Source Generator Samples");
+ Console.WriteLine("================================");
+
+ // Create a UserBasicFragment instance
+ var user = new UserBasicFragment
+ {
+ Id = "user-123",
+ Name = "John Doe",
+ Email = "john.doe@example.com",
+ IsActive = true
+ };
+
+ Console.WriteLine("\nUser Basic Fragment:");
+ Console.WriteLine($"ID: {user.Id}");
+ Console.WriteLine($"Name: {user.Name}");
+ Console.WriteLine($"Email: {user.Email}");
+ Console.WriteLine($"Active: {user.IsActive}");
+
+ // Create a PostWithStatsFragment instance
+ var post = new PostWithStatsFragment
+ {
+ Id = "post-123",
+ Title = "GraphQL and C# Source Generators",
+ ViewCount = 1250,
+ Rating = 4.8,
+ IsPublished = true,
+ PublishedAt = DateTime.Now.AddDays(-14),
+ Tags = new List { "GraphQL", "C#", "Source Generators" },
+ Categories = new List { "Programming", "Web Development" }
+ };
+
+ Console.WriteLine("\nPost With Stats Fragment:");
+ Console.WriteLine($"ID: {post.Id}");
+ Console.WriteLine($"Title: {post.Title}");
+ Console.WriteLine($"Views: {post.ViewCount}");
+ Console.WriteLine($"Rating: {post.Rating}");
+ Console.WriteLine($"Published: {post.IsPublished}");
+ Console.WriteLine($"Published At: {post.PublishedAt}");
+ Console.WriteLine($"Tags: {string.Join(", ", post.Tags ?? new List())}");
+ Console.WriteLine($"Categories: {string.Join(", ", post.Categories)}");
+
+ Console.WriteLine("\nPress any key to exit...");
+ Console.ReadKey();
+ }
+ }
+}
\ No newline at end of file
diff --git a/GraphQLSourceGen.Samples/README.md b/GraphQLSourceGen.Samples/README.md
new file mode 100644
index 0000000..e69de29
diff --git a/GraphQLSourceGen.Samples/schema.graphql b/GraphQLSourceGen.Samples/schema.graphql
new file mode 100644
index 0000000..6e4112a
--- /dev/null
+++ b/GraphQLSourceGen.Samples/schema.graphql
@@ -0,0 +1,78 @@
+# This is a sample GraphQL file with fragment definitions
+
+# A simple fragment with scalar fields
+fragment UserBasic on User {
+ id
+ name
+ email
+ isActive
+}
+
+# A fragment with nested objects and lists
+fragment UserDetails on User {
+ id
+ name
+ email
+ isActive
+ profile {
+ bio
+ avatarUrl
+ joinDate
+ }
+ posts {
+ id
+ title
+ content
+ createdAt
+ }
+ followers {
+ id
+ name
+ }
+}
+
+# A fragment with non-nullable fields
+fragment RequiredUserInfo on User {
+ id!
+ name!
+ email
+}
+
+# A fragment with deprecated fields
+fragment UserWithDeprecated on User {
+ id
+ name
+ email
+ username @deprecated(reason: "Use email instead")
+ oldField @deprecated
+}
+
+# A fragment that uses another fragment
+fragment UserWithPosts on User {
+ ...UserBasic
+ posts {
+ id
+ title
+ publishedAt
+ comments {
+ id
+ text
+ author {
+ id
+ name
+ }
+ }
+ }
+}
+
+# A fragment with various scalar types
+fragment PostWithStats on Post {
+ id: ID!
+ title: String!
+ viewCount: Int
+ rating: Float
+ isPublished: Boolean!
+ publishedAt: DateTime
+ tags: [String]
+ categories: [String!]!
+}
\ No newline at end of file
diff --git a/GraphQLSourceGen.Tests/GraphQLFragmentGeneratorTests.cs b/GraphQLSourceGen.Tests/GraphQLFragmentGeneratorTests.cs
index 239c378..b9a2461 100644
--- a/GraphQLSourceGen.Tests/GraphQLFragmentGeneratorTests.cs
+++ b/GraphQLSourceGen.Tests/GraphQLFragmentGeneratorTests.cs
@@ -28,7 +28,7 @@ fragment UserBasic on User {
var fragments = GraphQLParser.ParseContent(graphqlContent);
// Verify the fragment was parsed correctly
- Assert.Equal(1, fragments.Count);
+ Assert.Single(fragments);
var fragment = fragments[0];
Assert.Equal("UserBasic", fragment.Name);
diff --git a/GraphQLSourceGen.Tests/GraphQLParserTests.cs b/GraphQLSourceGen.Tests/GraphQLParserTests.cs
index 4e26518..8194c7f 100644
--- a/GraphQLSourceGen.Tests/GraphQLParserTests.cs
+++ b/GraphQLSourceGen.Tests/GraphQLParserTests.cs
@@ -23,7 +23,7 @@ fragment UserBasic on User {
var fragments = GraphQLParser.ParseContent(graphql);
// Assert
- Assert.Equal(1, fragments.Count);
+ Assert.Single(fragments);
var fragment = fragments[0];
Assert.Equal("UserBasic", fragment.Name);
@@ -53,7 +53,7 @@ fragment UserDetails on User {
var fragments = GraphQLParser.ParseContent(graphql);
// Assert
- Assert.Equal(1, fragments.Count);
+ Assert.Single(fragments);
var fragment = fragments[0];
Assert.Equal("UserDetails", fragment.Name);
@@ -82,7 +82,7 @@ oldField @deprecated
var fragments = GraphQLParser.ParseContent(graphql);
// Assert
- Assert.Equal(1, fragments.Count);
+ Assert.Single(fragments);
var fragment = fragments[0];
Assert.Equal(3, fragment.Fields.Count);
@@ -114,7 +114,7 @@ fragment UserWithPosts on User {
var fragments = GraphQLParser.ParseContent(graphql);
// Assert
- Assert.Equal(1, fragments.Count);
+ Assert.Single(fragments);
var fragment = fragments[0];
Assert.Equal(2, fragment.Fields.Count);
@@ -149,7 +149,7 @@ fragment PostWithStats on Post {
var fragments = GraphQLParser.ParseContent(graphql);
// Assert
- Assert.Equal(1, fragments.Count);
+ Assert.Single(fragments);
var fragment = fragments[0];
Assert.Equal(8, fragment.Fields.Count);
diff --git a/GraphQLSourceGen/Configuration/GraphQLSourceGenOptions.cs b/GraphQLSourceGen/Configuration/GraphQLSourceGenOptions.cs
new file mode 100644
index 0000000..da2d2ab
--- /dev/null
+++ b/GraphQLSourceGen/Configuration/GraphQLSourceGenOptions.cs
@@ -0,0 +1,30 @@
+using System;
+
+namespace GraphQLSourceGen.Configuration
+{
+ ///
+ /// Configuration options for GraphQL Source Generator
+ ///
+ public class GraphQLSourceGenOptions
+ {
+ ///
+ /// The namespace to use for generated types. If null, the namespace will be "GraphQL.Generated"
+ ///
+ public string? Namespace { get; set; }
+
+ ///
+ /// Whether to generate records (true) or classes (false)
+ ///
+ public bool UseRecords { get; set; } = true;
+
+ ///
+ /// Whether to use init-only properties
+ ///
+ public bool UseInitProperties { get; set; } = true;
+
+ ///
+ /// Whether to include XML documentation comments in generated code
+ ///
+ public bool GenerateDocComments { get; set; } = true;
+ }
+}
\ No newline at end of file
diff --git a/GraphQLSourceGen/Diagnostics/DiagnosticDescriptors.cs b/GraphQLSourceGen/Diagnostics/DiagnosticDescriptors.cs
new file mode 100644
index 0000000..2b32d30
--- /dev/null
+++ b/GraphQLSourceGen/Diagnostics/DiagnosticDescriptors.cs
@@ -0,0 +1,67 @@
+using Microsoft.CodeAnalysis;
+
+namespace GraphQLSourceGen.Diagnostics
+{
+ ///
+ /// Diagnostic descriptors for GraphQL Source Generator
+ ///
+ internal static class DiagnosticDescriptors
+ {
+ private const string AnalyzerReleaseTrackingId = "GQLSG";
+ ///
+ /// Invalid GraphQL syntax diagnostic
+ ///
+ public static readonly DiagnosticDescriptor InvalidGraphQLSyntax = new(
+ id: "GQLSG001",
+ title: "Invalid GraphQL syntax",
+ messageFormat: "The GraphQL file contains invalid syntax: {0}",
+ category: "GraphQLSourceGen",
+ DiagnosticSeverity.Error,
+ isEnabledByDefault: true,
+ description: "The GraphQL file contains syntax that could not be parsed. Fix the syntax error to generate code.",
+ helpLinkUri: $"https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md",
+ customTags: new[] { WellKnownDiagnosticTags.AnalyzerException });
+
+ ///
+ /// No GraphQL fragments found diagnostic
+ ///
+ public static readonly DiagnosticDescriptor NoFragmentsFound = new(
+ id: "GQLSG002",
+ title: "No GraphQL fragments found",
+ messageFormat: "No GraphQL fragments were found in the file {0}",
+ category: "GraphQLSourceGen",
+ DiagnosticSeverity.Warning,
+ isEnabledByDefault: true,
+ description: "The GraphQL file does not contain any fragment definitions. Add fragment definitions to generate code.",
+ helpLinkUri: $"https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md",
+ customTags: new[] { WellKnownDiagnosticTags.AnalyzerException });
+
+ ///
+ /// Invalid fragment name diagnostic
+ ///
+ public static readonly DiagnosticDescriptor InvalidFragmentName = new(
+ id: "GQLSG003",
+ title: "Invalid fragment name",
+ messageFormat: "The fragment name '{0}' is not a valid C# identifier",
+ category: "GraphQLSourceGen",
+ DiagnosticSeverity.Error,
+ isEnabledByDefault: true,
+ description: "The fragment name must be a valid C# identifier. Rename the fragment to a valid C# identifier.",
+ helpLinkUri: $"https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md",
+ customTags: new[] { WellKnownDiagnosticTags.AnalyzerException });
+
+ ///
+ /// Fragment spread not found diagnostic
+ ///
+ public static readonly DiagnosticDescriptor FragmentSpreadNotFound = new(
+ id: "GQLSG004",
+ title: "Fragment spread not found",
+ messageFormat: "The fragment spread '{0}' was not found in any of the GraphQL files",
+ category: "GraphQLSourceGen",
+ DiagnosticSeverity.Error,
+ isEnabledByDefault: true,
+ description: "The fragment spread references a fragment that was not found in any of the GraphQL files. Make sure the fragment is defined in one of the GraphQL files.",
+ helpLinkUri: $"https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md",
+ customTags: new[] { WellKnownDiagnosticTags.AnalyzerException });
+ }
+}
\ No newline at end of file
diff --git a/GraphQLSourceGen/GraphQLFragmentGenerator.cs b/GraphQLSourceGen/GraphQLFragmentGenerator.cs
index 36f51f8..62c6ec3 100644
--- a/GraphQLSourceGen/GraphQLFragmentGenerator.cs
+++ b/GraphQLSourceGen/GraphQLFragmentGenerator.cs
@@ -1,3 +1,5 @@
+using GraphQLSourceGen.Configuration;
+using GraphQLSourceGen.Diagnostics;
using GraphQLSourceGen.Models;
using GraphQLSourceGen.Parsing;
using Microsoft.CodeAnalysis;
@@ -16,6 +18,9 @@ public void Initialize(GeneratorInitializationContext context)
public void Execute(GeneratorExecutionContext context)
{
+ // Read configuration from MSBuild properties
+ var options = ReadConfiguration(context);
+
// Find all .graphql files in the project
var graphqlFiles = context.AdditionalFiles
.Where(file => file.Path.EndsWith(".graphql", StringComparison.OrdinalIgnoreCase))
@@ -31,35 +36,144 @@ public void Execute(GeneratorExecutionContext context)
var allFragments = new List();
foreach (var file in graphqlFiles)
{
- var fileContent = file.GetText()?.ToString() ?? string.Empty;
- var fragments = GraphQLParser.ParseFile(fileContent);
- allFragments.AddRange(fragments);
+ try
+ {
+ var fileContent = file.GetText()?.ToString() ?? string.Empty;
+ var fragments = GraphQLParser.ParseFile(fileContent);
+
+ if (!fragments.Any())
+ {
+ // Report diagnostic for no fragments found
+ var diagnostic = Diagnostic.Create(
+ DiagnosticDescriptors.NoFragmentsFound,
+ Location.None,
+ Path.GetFileName(file.Path));
+ context.ReportDiagnostic(diagnostic);
+ continue;
+ }
+
+ allFragments.AddRange(fragments);
+ }
+ catch (Exception ex)
+ {
+ // Report diagnostic for parsing error
+ var diagnostic = Diagnostic.Create(
+ DiagnosticDescriptors.InvalidGraphQLSyntax,
+ Location.None,
+ ex.Message);
+ context.ReportDiagnostic(diagnostic);
+ }
}
+ // Validate fragment names and references
+ ValidateFragments(context, allFragments);
+
// Generate code for each fragment
foreach (var fragment in allFragments)
{
- string generatedCode = GenerateFragmentCode(fragment, allFragments);
+ string generatedCode = GenerateFragmentCode(fragment, allFragments, options);
context.AddSource($"{fragment.Name}Fragment.g.cs", SourceText.From(generatedCode, Encoding.UTF8));
}
}
+
+ private void ValidateFragments(GeneratorExecutionContext context, List fragments)
+ {
+ // Check for invalid fragment names
+ foreach (var fragment in fragments)
+ {
+ if (!IsValidCSharpIdentifier(fragment.Name))
+ {
+ var diagnostic = Diagnostic.Create(
+ DiagnosticDescriptors.InvalidFragmentName,
+ Location.None,
+ fragment.Name);
+ context.ReportDiagnostic(diagnostic);
+ }
+ }
+
+ // Check for fragment spreads that don't exist
+ var fragmentNames = new HashSet(fragments.Select(f => f.Name));
+ foreach (var fragment in fragments)
+ {
+ foreach (var field in fragment.Fields)
+ {
+ foreach (var spread in field.FragmentSpreads)
+ {
+ if (!fragmentNames.Contains(spread))
+ {
+ var diagnostic = Diagnostic.Create(
+ DiagnosticDescriptors.FragmentSpreadNotFound,
+ Location.None,
+ spread);
+ context.ReportDiagnostic(diagnostic);
+ }
+ }
+ }
+ }
+ }
+
+ private bool IsValidCSharpIdentifier(string name)
+ {
+ if (string.IsNullOrEmpty(name))
+ return false;
+
+ if (!char.IsLetter(name[0]) && name[0] != '_')
+ return false;
+
+ for (int i = 1; i < name.Length; i++)
+ {
+ if (!char.IsLetterOrDigit(name[i]) && name[i] != '_')
+ return false;
+ }
+
+ return true;
+ }
+
+ private GraphQLSourceGenOptions ReadConfiguration(GeneratorExecutionContext context)
+ {
+ var options = new GraphQLSourceGenOptions();
+
+ if (context.AnalyzerConfigOptions.GlobalOptions.TryGetValue("build_property.GraphQLSourceGenNamespace", out var ns))
+ {
+ options.Namespace = ns;
+ }
+
+ if (context.AnalyzerConfigOptions.GlobalOptions.TryGetValue("build_property.GraphQLSourceGenUseRecords", out var useRecords))
+ {
+ options.UseRecords = bool.TryParse(useRecords, out var value) && value;
+ }
+
+ if (context.AnalyzerConfigOptions.GlobalOptions.TryGetValue("build_property.GraphQLSourceGenUseInitProperties", out var useInitProperties))
+ {
+ options.UseInitProperties = bool.TryParse(useInitProperties, out var value) && value;
+ }
+
+ if (context.AnalyzerConfigOptions.GlobalOptions.TryGetValue("build_property.GraphQLSourceGenGenerateDocComments", out var generateDocComments))
+ {
+ options.GenerateDocComments = bool.TryParse(generateDocComments, out var value) && value;
+ }
+
+ return options;
+ }
- string GenerateFragmentCode(GraphQLFragment fragment, List allFragments)
+ string GenerateFragmentCode(GraphQLFragment fragment, List allFragments, GraphQLSourceGenOptions options)
{
var sb = new StringBuilder();
- // Add using statements
+ // Add using statements and nullable directive
sb.AppendLine("using System;");
sb.AppendLine("using System.Collections.Generic;");
sb.AppendLine();
-
+ sb.AppendLine("#nullable enable");
+ sb.AppendLine();
// Add namespace
- sb.AppendLine("namespace GraphQL.Generated");
+ string ns = options.Namespace ?? "GraphQL.Generated";
+ sb.AppendLine($"namespace {ns}");
sb.AppendLine("{");
- // Generate the record
- GenerateClass(sb, fragment, allFragments, " ");
+ // Generate the class or record
+ GenerateClass(sb, fragment, allFragments, " ", options);
// Close namespace
sb.AppendLine("}");
@@ -67,45 +181,79 @@ string GenerateFragmentCode(GraphQLFragment fragment, List allF
return sb.ToString();
}
- void GenerateClass(StringBuilder sb, GraphQLFragment fragment, List allFragments, string indent)
+ void GenerateClass(StringBuilder sb, GraphQLFragment fragment, List allFragments, string indent, GraphQLSourceGenOptions options)
{
- // Class declaration
- sb.AppendLine($"{indent}/// ");
- sb.AppendLine($"{indent}/// Generated from GraphQL fragment '{fragment.Name}' on type '{fragment.OnType}'");
- sb.AppendLine($"{indent}/// ");
- sb.AppendLine($"{indent}public class {fragment.Name}Fragment");
+ // Class or record declaration
+ if (options.GenerateDocComments)
+ {
+ sb.AppendLine($"{indent}/// ");
+ sb.AppendLine($"{indent}/// Generated from GraphQL fragment '{fragment.Name}' on type '{fragment.OnType}'");
+ sb.AppendLine($"{indent}/// ");
+ }
+
+ string typeKeyword = options.UseRecords ? "record" : "class";
+ sb.AppendLine($"{indent}public {typeKeyword} {fragment.Name}Fragment");
sb.AppendLine($"{indent}{{");
// Generate properties for each field
foreach (var field in fragment.Fields)
{
- GenerateProperty(sb, field, allFragments, indent + " ");
+ GenerateProperty(sb, field, allFragments, indent + " ", options);
}
- // Generate nested records for complex fields
+ // Generate nested classes for complex fields
foreach (var field in fragment.Fields.Where(f => f.SelectionSet.Any()))
{
sb.AppendLine();
- var nestedFragment = new GraphQLFragment
+ string nestedTypeName = char.ToUpper(field.Name[0]) + field.Name.Substring(1);
+
+ if (options.GenerateDocComments)
+ {
+ sb.AppendLine($"{indent} /// ");
+ sb.AppendLine($"{indent} /// Represents the {field.Name} field of {fragment.Name}");
+ sb.AppendLine($"{indent} /// ");
+ }
+
+ string nestedTypeKeyword = options.UseRecords ? "record" : "class";
+ sb.AppendLine($"{indent} public {nestedTypeKeyword} {nestedTypeName}Model");
+ sb.AppendLine($"{indent} {{");
+
+ // Generate properties for nested fields
+ foreach (var nestedField in field.SelectionSet)
{
- Name = $"{fragment.Name}_{char.ToUpper(field.Name[0]) + field.Name.Substring(1)}",
- OnType = field.Type.Name,
- Fields = field.SelectionSet
- };
+ GenerateProperty(sb, nestedField, allFragments, indent + " ", options);
+ }
+
+ // Generate nested classes for nested fields with selection sets
+ foreach (var nestedField in field.SelectionSet.Where(f => f.SelectionSet.Any()))
+ {
+ sb.AppendLine();
+ var nestedFragment = new GraphQLFragment
+ {
+ Name = $"{nestedTypeName}_{char.ToUpper(nestedField.Name[0]) + nestedField.Name.Substring(1)}",
+ OnType = nestedField.Type.Name,
+ Fields = nestedField.SelectionSet
+ };
- GenerateClass(sb, nestedFragment, allFragments, indent + " ");
+ GenerateClass(sb, nestedFragment, allFragments, indent + " ", options);
+ }
+
+ sb.AppendLine($"{indent} }}");
}
- // Close record
+ // Close class or record
sb.AppendLine($"{indent}}}");
}
- void GenerateProperty(StringBuilder sb, GraphQLField field, List allFragments, string indent)
+ void GenerateProperty(StringBuilder sb, GraphQLField field, List allFragments, string indent, GraphQLSourceGenOptions options)
{
// Add XML documentation
- sb.AppendLine($"{indent}/// ");
- sb.AppendLine($"{indent}/// {field.Name}");
- sb.AppendLine($"{indent}/// ");
+ if (options.GenerateDocComments)
+ {
+ sb.AppendLine($"{indent}/// ");
+ sb.AppendLine($"{indent}/// {field.Name}");
+ sb.AppendLine($"{indent}/// ");
+ }
// Add [Obsolete] attribute if the field is deprecated
if (field.IsDeprecated)
@@ -120,18 +268,18 @@ void GenerateProperty(StringBuilder sb, GraphQLField field, List{(isNullable ? "?" : "")}";
+ propertyType = $"List<{typeName}Model>{(isNullable ? "?" : "")}";
}
else
{
- propertyType = $"{typeName}{(isNullable ? "?" : "")}";
+ propertyType = $"{typeName}Model{(isNullable ? "?" : "")}";
}
}
else if (field.FragmentSpreads.Any())
@@ -152,7 +300,8 @@ void GenerateProperty(StringBuilder sb, GraphQLField field, Listlatest
enable
enable
+
+
false
true
true
+ true
+
+
+ GraphQLSourceGen
+ 1.0.0
+ Your Name
+ A Roslyn-based C# Source Generator that creates C# types from GraphQL fragment definitions.
+ graphql;source-generator;codegen;roslyn
+ MIT
+ https://github.com/yourusername/graphql-sourcegen
+ https://github.com/yourusername/graphql-sourcegen
+ git
+ README.md
+
+
+ true
+ $(NoWarn);CS1591
@@ -15,8 +34,19 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/GraphQLSourceGen/build/GraphQLSourceGen.props b/GraphQLSourceGen/build/GraphQLSourceGen.props
new file mode 100644
index 0000000..a431a5d
--- /dev/null
+++ b/GraphQLSourceGen/build/GraphQLSourceGen.props
@@ -0,0 +1,9 @@
+
+
+
+ GraphQL.Generated
+ true
+ true
+ true
+
+
\ No newline at end of file
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..c6bc987
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2025 GraphQLSourceGen Contributors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
\ No newline at end of file
diff --git a/README.md b/README.md
index 64a3afc..5971e4e 100644
--- a/README.md
+++ b/README.md
@@ -1,25 +1,45 @@
# GraphQL Fragment Source Generator
-A Roslyn-based C# Source Generator that scans GraphQL files for fragment definitions and produces matching C# record types.
+A Roslyn-based C# Source Generator that scans GraphQL files for fragment definitions and produces matching C# types.
## Features
-- Automatically generates C# record types from GraphQL fragment definitions
+- Automatically generates C# types from GraphQL fragment definitions
- Preserves GraphQL scalar-to-C# type mappings
- Supports nullable reference types to reflect GraphQL nullability
-- Handles nested selections with nested record types
+- Handles nested selections with nested types
- Supports `@deprecated` directive with `[Obsolete]` attributes
- Handles fragment spreads through composition
+- Configurable output (namespace, records vs classes, etc.)
+- Comprehensive error reporting and diagnostics
## Installation
-Add a reference to the source generator project in your .NET 6+ SDK-style project:
+### NuGet Package (Recommended)
+
+Install the package from NuGet:
+
+```bash
+dotnet add package GraphQLSourceGen
+```
+
+Or add it directly to your project file:
+
+```xml
+
+
+
+```
+
+### Project Reference (For Development)
+
+If you're developing or customising the generator, add a reference to the source generator project:
```xml
+ ReferenceOutputAssembly="true" />
```
@@ -34,11 +54,31 @@ Add a reference to the source generator project in your .NET 6+ SDK-style projec
```
-3. The source generator will automatically process these files and generate C# record types for each fragment
+3. The source generator will automatically process these files and generate C# types for each fragment
-## Example
+## Configuration
-### GraphQL Fragment
+You can configure the generator using MSBuild properties in your project file:
+
+```xml
+
+
+ MyCompany.GraphQL.Generated
+
+
+ false
+
+
+ false
+
+
+ false
+
+```
+
+## Examples
+
+### Basic Fragment
```graphql
fragment UserBasic on User {
@@ -49,12 +89,14 @@ fragment UserBasic on User {
}
```
-### Generated C# Record
+Generated C# type:
```csharp
using System;
using System.Collections.Generic;
+#nullable enable
+
namespace GraphQL.Generated
{
///
@@ -84,3 +126,77 @@ namespace GraphQL.Generated
}
}
```
+
+### Fragment with Nested Objects
+
+```graphql
+fragment UserWithProfile on User {
+ id
+ name
+ profile {
+ bio
+ avatarUrl
+ joinDate
+ }
+}
+```
+
+### Fragment with Non-Nullable Fields
+
+```graphql
+fragment RequiredUserInfo on User {
+ id!
+ name!
+ email
+}
+```
+
+### Fragment with Deprecated Fields
+
+```graphql
+fragment UserWithDeprecated on User {
+ id
+ name
+ username @deprecated(reason: "Use email instead")
+ oldField @deprecated
+}
+```
+
+### Fragment that References Another Fragment
+
+```graphql
+fragment UserWithPosts on User {
+ ...UserBasic
+ posts {
+ id
+ title
+ }
+}
+```
+
+## Troubleshooting
+
+### Common Issues
+
+#### No Code is Generated
+
+Make sure:
+- Your `.graphql` files are marked as `AdditionalFiles` in your project file
+- Your GraphQL files contain valid fragment definitions
+- You've added the package reference correctly
+
+#### Error GQLSG001: Invalid GraphQL syntax
+
+This indicates a syntax error in your GraphQL file. Check the error message for details on the specific issue.
+
+#### Error GQLSG002: No GraphQL fragments found
+
+The GraphQL file doesn't contain any fragment definitions. Make sure you're using the `fragment Name on Type { ... }` syntax.
+
+## Contributing
+
+Contributions are welcome! Please feel free to submit a Pull Request.
+
+## License
+
+This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
diff --git a/graphql-sourcegen.sln b/graphql-sourcegen.sln
index c5e2079..722b47e 100644
--- a/graphql-sourcegen.sln
+++ b/graphql-sourcegen.sln
@@ -6,6 +6,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GraphQLSourceGen.Tests", "G
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GraphQLSourceGen", "GraphQLSourceGen\GraphQLSourceGen.csproj", "{3517AEAF-A318-D15F-A9CC-DDF3639CC089}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GraphQLSourceGen.Samples", "GraphQLSourceGen.Samples\GraphQLSourceGen.Samples.csproj", "{B4C5D6E7-F8A9-0B1C-2D3E-4F5A6B7C8D9E}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -20,6 +22,10 @@ Global
{3517AEAF-A318-D15F-A9CC-DDF3639CC089}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3517AEAF-A318-D15F-A9CC-DDF3639CC089}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3517AEAF-A318-D15F-A9CC-DDF3639CC089}.Release|Any CPU.Build.0 = Release|Any CPU
+ {B4C5D6E7-F8A9-0B1C-2D3E-4F5A6B7C8D9E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {B4C5D6E7-F8A9-0B1C-2D3E-4F5A6B7C8D9E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {B4C5D6E7-F8A9-0B1C-2D3E-4F5A6B7C8D9E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {B4C5D6E7-F8A9-0B1C-2D3E-4F5A6B7C8D9E}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE