Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions src/Trax.Api.GraphQL/Configuration/GraphQLConfiguration.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
namespace Trax.Api.GraphQL.Configuration;

/// <summary>
/// Holds the resolved configuration for the Trax GraphQL schema,
/// including discovered query model registrations.
/// </summary>
public class GraphQLConfiguration
{
public IReadOnlyList<QueryModelRegistration> ModelRegistrations { get; }

/// <summary>
/// Tracks which namespace base types and namespace fields have been registered
/// across type modules to prevent duplicate registrations. Populated at runtime
/// by <c>TrainTypeModule</c> and <c>QueryModelTypeModule</c>.
/// </summary>
internal HashSet<string> RegisteredNamespaceTypes { get; } = new(StringComparer.Ordinal);

public GraphQLConfiguration(IReadOnlyList<QueryModelRegistration> modelRegistrations)
{
ModelRegistrations = modelRegistrations;
}
}
13 changes: 13 additions & 0 deletions src/Trax.Api.GraphQL/Configuration/QueryModelRegistration.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using Trax.Effect.Attributes;

namespace Trax.Api.GraphQL.Configuration;

/// <summary>
/// Represents a discovered entity type marked with <see cref="TraxQueryModelAttribute"/>
/// and its owning DbContext type.
/// </summary>
public record QueryModelRegistration(
Type EntityType,
Type DbContextType,
TraxQueryModelAttribute Attribute
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using System.Reflection;
using Microsoft.EntityFrameworkCore;
using Trax.Effect.Attributes;

namespace Trax.Api.GraphQL.Configuration.TraxGraphQLBuilder;

public partial class TraxGraphQLBuilder
{
internal GraphQLConfiguration Build()
{
var modelRegistrations = new List<QueryModelRegistration>();

foreach (var dbContextType in DbContextTypes)
{
var dbSetProps = dbContextType
.GetProperties(BindingFlags.Public | BindingFlags.Instance)
.Where(p =>
p.PropertyType.IsGenericType
&& p.PropertyType.GetGenericTypeDefinition() == typeof(DbSet<>)
);

foreach (var prop in dbSetProps)
{
var entityType = prop.PropertyType.GetGenericArguments()[0];
var attr = entityType.GetCustomAttribute<TraxQueryModelAttribute>();
if (attr is null)
continue;

modelRegistrations.Add(new QueryModelRegistration(entityType, dbContextType, attr));
}
}

return new GraphQLConfiguration(modelRegistrations);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using Microsoft.EntityFrameworkCore;

namespace Trax.Api.GraphQL.Configuration.TraxGraphQLBuilder;

public partial class TraxGraphQLBuilder
{
/// <summary>
/// Registers a DbContext whose <c>DbSet&lt;T&gt;</c> entities marked with
/// <c>[TraxQueryModel]</c> will be automatically exposed as paginated,
/// filterable, sortable GraphQL queries under <c>discover</c>.
/// </summary>
/// <typeparam name="TDbContext">
/// The DbContext type containing DbSet properties for the entities to expose.
/// Must be registered in DI (e.g. via <c>AddDbContextFactory</c> or <c>AddDbContext</c>).
/// </typeparam>
public TraxGraphQLBuilder AddDbContext<TDbContext>()
where TDbContext : DbContext
{
DbContextTypes.Add(typeof(TDbContext));
return this;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using System.ComponentModel;
using Microsoft.Extensions.DependencyInjection;

namespace Trax.Api.GraphQL.Configuration.TraxGraphQLBuilder;

/// <summary>
/// Builder for configuring the Trax GraphQL schema, including DbContext-based
/// model query registration.
/// </summary>
public partial class TraxGraphQLBuilder
{
[EditorBrowsable(EditorBrowsableState.Never)]
internal IServiceCollection Services { get; }

internal List<Type> DbContextTypes { get; } = [];

public TraxGraphQLBuilder(IServiceCollection services)
{
Services = services;
}
}
63 changes: 58 additions & 5 deletions src/Trax.Api.GraphQL/Extensions/GraphQLServiceExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
using HotChocolate.Data;
using HotChocolate.Types;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Trax.Api.Extensions;
using Trax.Api.GraphQL.Configuration;
using Trax.Api.GraphQL.Configuration.TraxGraphQLBuilder;
using Trax.Api.GraphQL.Errors;
using Trax.Api.GraphQL.Hooks;
using Trax.Api.GraphQL.Mutations;
Expand All @@ -20,18 +23,31 @@ public static class GraphQLServiceExtensions
private const string SchemaName = "trax";

/// <summary>
/// Registers the Trax GraphQL schema on a named HotChocolate server ("trax").
/// This avoids conflicts with a consumer's own default GraphQL schema.
/// Only trains annotated with <c>[TraxQuery]</c> or <c>[TraxMutation]</c> get typed operations generated.
/// Registers the Trax GraphQL schema on a named HotChocolate server ("trax")
/// with support for configuring DbContext-based model queries.
/// </summary>
public static IServiceCollection AddTraxGraphQL(this IServiceCollection services)
/// <example>
/// <code>
/// services.AddTraxGraphQL(graphql => graphql
/// .AddDbContext&lt;GameDbContext&gt;());
/// </code>
/// </example>
public static IServiceCollection AddTraxGraphQL(
this IServiceCollection services,
Func<TraxGraphQLBuilder, TraxGraphQLBuilder> configure
)
{
if (!services.Any(sd => sd.ServiceType == typeof(TraxMarker)))
throw new InvalidOperationException(
"AddTraxGraphQL() requires AddTrax() to be called first. "
+ "Call services.AddTrax(trax => ...) before services.AddTraxGraphQL()."
);

var builder = new TraxGraphQLBuilder(services);
configure(builder);
var config = builder.Build();
services.AddSingleton(config);

services.AddTraxApi();
services.AddSingleton<TrainTypeModule>();
services.AddTransient<GraphQLSubscriptionHook>();
Expand All @@ -40,7 +56,8 @@ public static IServiceCollection AddTraxGraphQL(this IServiceCollection services
.AddSingleton<ITrainLifecycleHookFactory>(sp =>
sp.GetRequiredService<GraphQLSubscriptionHookFactory>()
);
services

var graphqlBuilder = services
.AddGraphQLServer(SchemaName)
.AddQueryType<RootQuery>()
.AddMutationType<RootMutation>()
Expand All @@ -52,6 +69,34 @@ public static IServiceCollection AddTraxGraphQL(this IServiceCollection services
.AddErrorFilter<TraxErrorFilter>()
.AddInMemorySubscriptions();

if (config.ModelRegistrations.Count > 0)
{
services.AddSingleton<QueryModelTypeModule>();
graphqlBuilder.AddTypeModule<QueryModelTypeModule>();

// Register DiscoverQueries base type and discover field on RootQuery.
// TrainTypeModule will skip creating these when it detects model registrations.
graphqlBuilder.AddType(new ObjectType<DiscoverQueries>());
graphqlBuilder.AddTypeExtension(
new ObjectTypeExtension(d =>
{
d.Name("RootQuery");
d.Field("discover")
.Type<ObjectType<DiscoverQueries>>()
.Resolve(_ => new DiscoverQueries());
})
);

if (config.ModelRegistrations.Any(r => r.Attribute.Filtering))
graphqlBuilder.AddFiltering();

if (config.ModelRegistrations.Any(r => r.Attribute.Sorting))
graphqlBuilder.AddSorting();

if (config.ModelRegistrations.Any(r => r.Attribute.Projection))
graphqlBuilder.AddProjections();
}

// If a broadcaster receiver is registered (via UseBroadcaster()),
// wire up the GraphQL handler so remote lifecycle events are forwarded
// to HotChocolate subscriptions.
Expand All @@ -63,6 +108,14 @@ public static IServiceCollection AddTraxGraphQL(this IServiceCollection services
return services;
}

/// <summary>
/// Registers the Trax GraphQL schema on a named HotChocolate server ("trax").
/// This avoids conflicts with a consumer's own default GraphQL schema.
/// Only trains annotated with <c>[TraxQuery]</c> or <c>[TraxMutation]</c> get typed operations generated.
/// </summary>
public static IServiceCollection AddTraxGraphQL(this IServiceCollection services) =>
services.AddTraxGraphQL(builder => builder);

/// <summary>
/// Maps the Trax GraphQL endpoint at the specified route prefix.
/// Uses a named schema so it coexists with other HotChocolate schemas
Expand Down
4 changes: 3 additions & 1 deletion src/Trax.Api.GraphQL/Trax.Api.GraphQL.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@

<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="HotChocolate.AspNetCore" Version="14.*" />
<PackageReference Include="HotChocolate.AspNetCore" Version="15.*" />
<PackageReference Include="HotChocolate.Data" Version="15.*" />
<PackageReference Include="HotChocolate.Data.EntityFramework" Version="15.*" />
</ItemGroup>
</Project>
Loading
Loading