Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Split Construction of Graph Types into individual modules #121

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
69 changes: 69 additions & 0 deletions src/AspNetCoreMulti/Example/GraphQL/Operations.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
using Example.Repositories;
using GraphQL.Types;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Collections.Generic;

namespace Example.GraphQL
{
public interface IOperation
{
IEnumerable<IFieldType> RegisterFields();
}

public interface IDogOperation : IOperation { }

public interface ICatOperation : IOperation { }

public class DogOperation : ObjectGraphType<object>, IDogOperation
{
private readonly IServiceProvider _serviceProvider;

public DogOperation(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}

public IEnumerable<IFieldType> RegisterFields()
{
var fields = new List<IFieldType>
{
Field<StringGraphType>("say", resolve: context => "woof woof woof"),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Field<StringGraphType>( appends field to graph type and returns reference to it.

GetBreedListField(),
GetImageDetailsField()
};

return fields;
}

private IFieldType GetBreedListField()
{
return Field<NonNullGraphType<ListGraphType<NonNullGraphType<DogType>>>>("dogBreeds", resolve: context =>
{
using var scope = _serviceProvider.CreateScope();
var dogRepository = scope.ServiceProvider.GetRequiredService<DogRepository>();
var dogs = dogRepository.GetDogs();
return dogs;
});
}

private IFieldType GetImageDetailsField()
{
return FieldAsync<NonNullGraphType<ImageDetailsType>>("dogImageDetails", resolve: async context =>
{
using var scope = _serviceProvider.CreateScope();
var imageDetailsRepository = scope.ServiceProvider.GetRequiredService<DogImageDetailsRepository>();
var imageDetails = await imageDetailsRepository.GetDogImageDetails();
return imageDetails;
});
}
}

public class CatSayOperation : ObjectGraphType<object>, ICatOperation
{
public IEnumerable<IFieldType> RegisterFields()
{
return new List<IFieldType> { Field<StringGraphType>("say", resolve: context => "meow meow meow") };
sungam3r marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
38 changes: 38 additions & 0 deletions src/AspNetCoreMulti/Example/GraphQL/Queries.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using GraphQL.Types;
using System.Collections.Generic;

namespace Example.GraphQL
{
public class Queries
{
public class DogQuery : ObjectGraphType<object>
{
public DogQuery(IEnumerable<IDogOperation> dogOperations)
{
foreach(var dogOperation in dogOperations)
sungam3r marked this conversation as resolved.
Show resolved Hide resolved
{
var fields = dogOperation.RegisterFields();
foreach(var field in fields)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
foreach(var field in fields)
foreach (var field in fields)

{
AddField((FieldType)field);
}
}
}
}

public class CatQuery : ObjectGraphType<object>
{
public CatQuery(IEnumerable<ICatOperation> catOperations)
{
foreach(var catOperation in catOperations)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
foreach(var catOperation in catOperations)
foreach (var catOperation in catOperations)

{
var fields = catOperation.RegisterFields();
foreach (var field in fields)
{
AddField((FieldType)field);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe it's better to use AutoSchema here.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you give me some more details here? Do you have any doc handy for AutoSchema? I'm thinking it is some way to set things up automatically.

One reason I am doing the way I am right now, is to be non-opinionated on the setup. I give control to the implementor of IQuery to give me the fields as they see fit. They can subsequently use reflection or any other controlled mechanism to set things up and finally return the fields which I would then attach to the root query. This is especially useful when the application is large, and consumers need several ways to opt in/out of things.

Again, I might be talking about something different but will wait for information on AutoSchema.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not really sure how the use of AutoSchema fits into your example. AutoSchema is useful when creating a type-first schema, where the schema is automatically constructed from CLR types. Your sample does not use those features. Moreover, you are attempting to use methods from multiple classes to assemble a large root graph type. An auto-generated root query type would be contrary to that goal.

Copy link
Member

@Shane32 Shane32 Feb 1, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can see a demonstration of a type-first schema here:

https://github.com/graphql-dotnet/graphql-dotnet/tree/master/src/GraphQL.StarWars.TypeFirst

The entire schema can be constructed via:

services.AddGraphQL(b => b
    .AddSystemTextJson()
    .AddAutoSchema<StarWarsQuery>(c => c.WithMutation<StarWarsMutation>()));

You will notice there are no 'graph types' but rather simple CLR classes decorated with attributes where necessary

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my own graphs, I use a slight variation of this to allow for DI injection of scoped services, which is not possible with the sample seen there. However, the code exists in the tests here:

https://github.com/graphql-dotnet/graphql-dotnet/blob/master/src/GraphQL.Tests/Bugs/Issue2932_DemoDIGraphType.cs

I also published it as a NuGet package here:

https://www.nuget.org/packages/Shane32.GraphQL.DI

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@EmmanuelPonnudurai I'm fine with example as is without AutoSchema.

}
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
using GraphQL.Types;
using System;
using static Example.GraphQL.Queries;

namespace Example
namespace Example.GraphQL
{
public class DogSchema : Schema
{
Expand All @@ -12,14 +13,6 @@ public DogSchema(IServiceProvider provider, DogQuery query)
}
}

public class DogQuery : ObjectGraphType<object>
{
public DogQuery()
{
Field<StringGraphType>("say", resolve: context => "woof woof woof");
}
}

public class CatSchema : Schema
{
public CatSchema(IServiceProvider provider, CatQuery query)
Expand All @@ -28,12 +21,4 @@ public CatSchema(IServiceProvider provider, CatQuery query)
Query = query;
}
}

public class CatQuery : ObjectGraphType<object>
{
public CatQuery()
{
Field<StringGraphType>("say", resolve: context => "meow meow meow");
}
}
}
28 changes: 28 additions & 0 deletions src/AspNetCoreMulti/Example/GraphQL/Types.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using GraphQL.Types;

namespace Example.GraphQL
{
public class DogType : ObjectGraphType<Dog>
{
public DogType()
{
Field(x => x.Breed);
}
}

public class CatType : ObjectGraphType<Dog>
{
public CatType()
{
Field(x => x.Breed);
}
}

public class ImageDetailsType : ObjectGraphType<ImageDetails>
{
public ImageDetailsType()
{
Field(x => x.Url);
}
}
}
17 changes: 17 additions & 0 deletions src/AspNetCoreMulti/Example/Models.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
namespace Example
{
public class Dog
{
public string Breed { get; set; }
}

public class Cat
{
public string Breed { get; set; }
sungam3r marked this conversation as resolved.
Show resolved Hide resolved
}

public class ImageDetails
{
public string Url { get; set; }
}
}
22 changes: 17 additions & 5 deletions src/AspNetCoreMulti/Example/Program.cs
Original file line number Diff line number Diff line change
@@ -1,15 +1,27 @@
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;
using System;
using System.Threading.Tasks;

namespace Example
{
public class Program
{
public static Task Main(string[] args) => Host
.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(builder => builder.UseStartup<Startup>())
.Build()
.RunAsync();
public static Task Main(string[] args)
{
try
{
return Host
.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(builder => builder.UseStartup<Startup>())
.Build()
.RunAsync();
}
catch (System.Exception ex)
{
Console.WriteLine(ex);
return Task.FromResult(0);
}
sungam3r marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
17 changes: 17 additions & 0 deletions src/AspNetCoreMulti/Example/Repositories/CatRepository.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using System.Collections.Generic;
using System.Linq;

namespace Example.Repositories
{
public class CatRepository
{
private static readonly List<Cat> Cats = new()
{
new Cat{ Breed = "Abyssinian" },
new Cat{ Breed = "American Bobtail" },
new Cat{ Breed = "Burmese" }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
new Cat{ Breed = "Abyssinian" },
new Cat{ Breed = "American Bobtail" },
new Cat{ Breed = "Burmese" }
new Cat { Breed = "Abyssinian" },
new Cat { Breed = "American Bobtail" },
new Cat { Breed = "Burmese" }

};

public IEnumerable<Cat> GetCats() => Cats.AsEnumerable();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using System.Net.Http;
using System.Threading.Tasks;

namespace Example.Repositories
{
public class DogImageDetailsRepository
{
private readonly IHttpClientFactory _httpClientFactory;

public DogImageDetailsRepository(IHttpClientFactory httpClientFactory)
{
_httpClientFactory = httpClientFactory;
}

public async Task<ImageDetails> GetDogImageDetails()
{
var client = _httpClientFactory.CreateClient("DogsApi");
var result = await client.GetStringAsync("api/breeds/image/random");

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change

return new ImageDetails { Url = result };
}
}
}
17 changes: 17 additions & 0 deletions src/AspNetCoreMulti/Example/Repositories/DogRepository.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using System.Collections.Generic;
using System.Linq;

namespace Example.Repositories
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use file-scoped namespaces. We will convert codebase to use filescoped namespaces some time.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean only for new files. Or just leave it as is, not a big deal.

{
public class DogRepository
{
private static readonly List<Dog> Dogs = new()
{
new Dog{ Breed = "Doberman" },
new Dog{ Breed = "Pit Bull" },
new Dog{ Breed = "German Shepard" }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
new Dog{ Breed = "Doberman" },
new Dog{ Breed = "Pit Bull" },
new Dog{ Breed = "German Shepard" }
new Dog { Breed = "Doberman" },
new Dog { Breed = "Pit Bull" },
new Dog { Breed = "German Shepard" }

};

public IEnumerable<Dog> GetDogs() => Dogs.AsEnumerable();
}
}
21 changes: 21 additions & 0 deletions src/AspNetCoreMulti/Example/Startup.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using Example.GraphQL;
using Example.Repositories;
using GraphQL;
using GraphQL.MicrosoftDI;
using GraphQL.Server;
Expand All @@ -16,6 +18,12 @@ public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddScoped<DogRepository>();
services.AddScoped<DogImageDetailsRepository>();
services.AddScoped<CatRepository>();

services.AddOperations();

services.AddGraphQL(b => b
.AddHttpMiddleware<DogSchema>()
.AddHttpMiddleware<CatSchema>()
Expand All @@ -28,6 +36,10 @@ public void ConfigureServices(IServiceCollection services)

services.AddLogging(builder => builder.AddConsole());
services.AddHttpContextAccessor();
services.AddHttpClient("DogsApi", x =>
{
x.BaseAddress = new System.Uri("https://dog.ceo/");
});
}

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
Expand All @@ -40,6 +52,15 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env)

app.UseGraphQLPlayground(new PlaygroundOptions { GraphQLEndPoint = "/api/dogs" }, "/ui/dogs");
app.UseGraphQLPlayground(new PlaygroundOptions { GraphQLEndPoint = "/api/cats" }, "/ui/cats");
}
}

public static class StartupExtensions
{
public static void AddOperations(this IServiceCollection services)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would inline this method into ConfigureServices.

{
services.AddSingleton<IDogOperation, DogOperation>();
services.AddSingleton<ICatOperation, CatSayOperation>();
}
}
}