Skip to content

Commit

Permalink
Add auto input object graph type and builder extensions
Browse files Browse the repository at this point in the history
  • Loading branch information
Shane32 committed Nov 4, 2021
1 parent a59edf3 commit 2ad401e
Show file tree
Hide file tree
Showing 15 changed files with 684 additions and 197 deletions.
2 changes: 1 addition & 1 deletion Sample/Sample.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

<ItemGroup>
<PackageReference Include="EfLocalDb" Version="8.5.0" />
<PackageReference Include="GraphQL.MicrosoftDI" Version="4.5.0" />
<PackageReference Include="GraphQL.MicrosoftDI" Version="4.6.0" />
<PackageReference Include="GraphQL.Server.Transports.AspNetCore.SystemTextJson" Version="5.0.2" />
<PackageReference Include="GraphQL.Server.Ui.GraphiQL" Version="5.0.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="3.1.16" />
Expand Down
158 changes: 158 additions & 0 deletions src/GraphQL.DI/AutoInputObjectGraphType.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
using System.Text;
using GraphQL.Types;

namespace GraphQL.DI
{
/// <summary>
/// An automatically-generated graph type for public properties on the specified input model.
/// </summary>
public class AutoInputObjectGraphType<TSourceType> : InputObjectGraphType<TSourceType>
{
private const string ORIGINAL_EXPRESSION_PROPERTY_NAME = nameof(ORIGINAL_EXPRESSION_PROPERTY_NAME);

/// <summary>
/// Creates a GraphQL type from <typeparamref name="TSourceType"/>.
/// </summary>
public AutoInputObjectGraphType()
{
foreach (var property in GetRegisteredProperties()) {
var fieldType = ProcessProperty(property);
if (fieldType != null)
AddField(fieldType);
}
}

/// <summary>
/// Returns a list of properties that should have fields created for them.
/// </summary>
protected virtual IEnumerable<PropertyInfo> GetRegisteredProperties()
=> typeof(TSourceType).GetProperties(BindingFlags.Public | BindingFlags.Instance);

/// <summary>
/// Processes the specified property and returns a <see cref="FieldType"/>
/// </summary>
/// <param name="property"></param>
/// <returns></returns>
protected virtual FieldType? ProcessProperty(PropertyInfo property)
{
//get the field name
string fieldName = property.Name;
var fieldNameAttribute = property.GetCustomAttribute<NameAttribute>();
if (fieldNameAttribute != null) {
fieldName = fieldNameAttribute.Name;
}
if (fieldName == null)
return null; //ignore field if set to null

//determine the graphtype of the field
var graphTypeAttribute = property.GetCustomAttribute<GraphTypeAttribute>();
Type? graphType = graphTypeAttribute?.Type;
//infer the graphtype if it is not specified
if (graphType == null) {
graphType = InferGraphType(ApplyAttributes(GetTypeInformation(property)));
}
//load the description
string? description = property.GetCustomAttribute<DescriptionAttribute>()?.Description;
//load the deprecation reason
string? obsoleteDescription = property.GetCustomAttribute<ObsoleteAttribute>()?.Message;
//load the default value
object? defaultValue = property.GetCustomAttribute<DefaultValueAttribute>()?.Value;
//create the field
var fieldType = new FieldType() {
Type = graphType,
Name = fieldName,
Description = description,
DeprecationReason = obsoleteDescription,
DefaultValue = defaultValue,
};
//set name of property
fieldType.WithMetadata(ORIGINAL_EXPRESSION_PROPERTY_NAME, property.Name);
//load the metadata
foreach (var metaAttribute in property.GetCustomAttributes<MetadataAttribute>())
fieldType.WithMetadata(metaAttribute.Key, metaAttribute.Value);
//return the field
return fieldType;
}

/// <inheritdoc cref="ReflectionExtensions.GetNullabilityInformation(ParameterInfo)"/>
protected virtual IEnumerable<(Type Type, Nullability Nullable)> GetNullabilityInformation(PropertyInfo parameter)
{
return parameter.GetNullabilityInformation();
}

private static readonly Type[] _listTypes = new Type[] {
typeof(IEnumerable<>),
typeof(IList<>),
typeof(List<>),
typeof(ICollection<>),
typeof(IReadOnlyCollection<>),
typeof(IReadOnlyList<>),
typeof(HashSet<>),
typeof(ISet<>),
};

/// <summary>
/// Analyzes a property and returns a <see cref="TypeInformation"/>
/// struct containing type information necessary to select a graph type.
/// </summary>
protected virtual TypeInformation GetTypeInformation(PropertyInfo propertyInfo)
{
var isList = false;
var isNullableList = false;
var typeTree = GetNullabilityInformation(propertyInfo);
foreach (var type in typeTree) {
if (type.Type.IsArray) {
//unwrap type and mark as list
isList = true;
isNullableList = type.Nullable != Nullability.NonNullable;
continue;
}
if (type.Type.IsGenericType) {
var g = type.Type.GetGenericTypeDefinition();
if (_listTypes.Contains(g)) {
//unwrap type and mark as list
isList = true;
isNullableList = type.Nullable != Nullability.NonNullable;
continue;
}
}
if (type.Type == typeof(IEnumerable) || type.Type == typeof(ICollection)) {
//assume list of nullable object
isList = true;
isNullableList = type.Nullable != Nullability.NonNullable;
break;
}
//found match
var nullable = type.Nullable != Nullability.NonNullable;
return new TypeInformation(propertyInfo, true, type.Type, nullable, isList, isNullableList, null);
}
//unknown type
if (isList)
isNullableList = true;
return new TypeInformation(propertyInfo, true, typeof(object), true, isList, isNullableList, null);
}

/// <summary>
/// Apply <see cref="RequiredAttribute"/>, <see cref="OptionalAttribute"/>, <see cref="RequiredListAttribute"/>,
/// <see cref="OptionalListAttribute"/>, <see cref="IdAttribute"/> and <see cref="DIGraphAttribute"/> over
/// the supplied <see cref="TypeInformation"/>.
/// Override this method to enforce specific graph types for specific CLR types, or to implement custom
/// attributes to change graph type selection behavior.
/// </summary>
protected virtual TypeInformation ApplyAttributes(TypeInformation typeInformation)
=> typeInformation.ApplyAttributes(typeInformation.MemberInfo);

/// <summary>
/// Returns a GraphQL input type for a specified CLR type
/// </summary>
protected virtual Type InferGraphType(TypeInformation typeInformation)
=> typeInformation.InferGraphType();

}
}
23 changes: 9 additions & 14 deletions src/GraphQL.DI/DIObjectGraphBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,26 +12,21 @@ namespace GraphQL.DI
/// This is a required base type of all DI-created graph types. <see cref="DIObjectGraphBase"/> may be
/// used if the <see cref="IResolveFieldContext.Source"/> type is <see cref="object"/>.
/// </summary>
//this class is a placeholder for future support of properties or methods on the base class
public abstract class DIObjectGraphBase<TSource> : IDIObjectGraphBase<TSource>, IResolveFieldContext<TSource>
{
//this would be an ideal spot to put public readonly fields for the resolving query, such as Schema, Metrics, Executor, and so on, rather than being inside the ResolveFieldContext instance.
//this could only contain fields that are not unique to a resolving field (such as Source), so as to not break multithreading support
//with DI, any objects necessary could be brought in via dependency injection (such as Schema), so they really don't need to be in here

/// <summary>
/// The <see cref="IResolveFieldContext"/> for the current field.
/// </summary>
public IResolveFieldContext Context { get; private set; } = null!;

/// <inheritdoc cref="IResolveFieldContext.Source"/>
public TSource Source => (TSource)Context.Source;
public TSource Source => (TSource)Context.Source!;

/// <inheritdoc cref="IResolveFieldContext.CancellationToken"/>
public CancellationToken RequestAborted => Context.CancellationToken;

/// <inheritdoc cref="IProvideUserContext.UserContext"/>
public IDictionary<string, object> UserContext => Context.UserContext;
public IDictionary<string, object?> UserContext => Context.UserContext;

/// <inheritdoc cref="IResolveFieldContext.Metrics"/>
public Metrics Metrics => Context.Metrics;
Expand All @@ -40,10 +35,10 @@ public abstract class DIObjectGraphBase<TSource> : IDIObjectGraphBase<TSource>,
Field IResolveFieldContext.FieldAst => Context.FieldAst;
FieldType IResolveFieldContext.FieldDefinition => Context.FieldDefinition;
IObjectGraphType IResolveFieldContext.ParentType => Context.ParentType;
IResolveFieldContext IResolveFieldContext.Parent => Context.Parent;
IDictionary<string, ArgumentValue> IResolveFieldContext.Arguments => Context.Arguments;
object IResolveFieldContext.RootValue => Context.RootValue;
object IResolveFieldContext.Source => Context.Source;
IResolveFieldContext IResolveFieldContext.Parent => Context.Parent!;
IDictionary<string, ArgumentValue>? IResolveFieldContext.Arguments => Context.Arguments;
object? IResolveFieldContext.RootValue => Context.RootValue;
object IResolveFieldContext.Source => Context.Source!;
ISchema IResolveFieldContext.Schema => Context.Schema;
Document IResolveFieldContext.Document => Context.Document;
Operation IResolveFieldContext.Operation => Context.Operation;
Expand All @@ -52,9 +47,9 @@ public abstract class DIObjectGraphBase<TSource> : IDIObjectGraphBase<TSource>,
ExecutionErrors IResolveFieldContext.Errors => Context.Errors;
IEnumerable<object> IResolveFieldContext.Path => Context.Path;
IEnumerable<object> IResolveFieldContext.ResponsePath => Context.ResponsePath;
Dictionary<string, Field> IResolveFieldContext.SubFields => Context.SubFields;
IDictionary<string, object> IResolveFieldContext.Extensions => Context.Extensions;
IServiceProvider IResolveFieldContext.RequestServices => Context.RequestServices;
Dictionary<string, Field>? IResolveFieldContext.SubFields => Context.SubFields;
IDictionary<string, object?> IResolveFieldContext.Extensions => Context.Extensions;
IServiceProvider IResolveFieldContext.RequestServices => Context.RequestServices!;
IExecutionArrayPool IResolveFieldContext.ArrayPool => Context.ArrayPool;
}

Expand Down
Loading

0 comments on commit 2ad401e

Please sign in to comment.