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

Add DIGraphAttribute, AutoInputObjectGraphType, GraphQLBuilderExtensions #12

Merged
merged 8 commits into from
Nov 5, 2021
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
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
198 changes: 198 additions & 0 deletions src/GraphQL.DI/AutoInputObjectGraphType.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
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()
{
var classType = typeof(TSourceType);

//allow default name / description / obsolete tags to remain if not overridden
var nameAttribute = classType.GetCustomAttribute<NameAttribute>();
if (nameAttribute != null)
Name = nameAttribute.Name;
else {
var name = GetDefaultName();
if (name != null)
Name = name;
}

var descriptionAttribute = classType.GetCustomAttribute<DescriptionAttribute>();
if (descriptionAttribute != null)
Description = descriptionAttribute.Description;
var obsoleteAttribute = classType.GetCustomAttribute<ObsoleteAttribute>();
if (obsoleteAttribute != null)
DeprecationReason = obsoleteAttribute.Message;
//pull metadata
foreach (var metadataAttribute in classType.GetCustomAttributes<MetadataAttribute>())
Metadata.Add(metadataAttribute.Key, metadataAttribute.Value);

foreach (var property in GetRegisteredProperties()) {
if (property != null) {
var fieldType = ProcessProperty(property);
if (fieldType != null)
AddField(fieldType);

}
}
}

/// <summary>
/// Returns the default name assigned to the graph, or <see langword="null"/> to leave the default setting set by the <see cref="GraphType"/> constructor.
/// </summary>
protected virtual string? GetDefaultName()
{
//if this class is inherited, do not set default name
if (GetType() != typeof(AutoInputObjectGraphType<TSourceType>))
return null;

//without this code, the name would default to AutoInputObjectGraphType_1
var name = typeof(TSourceType).Name.Replace('`', '_');
if (name.EndsWith("Model", StringComparison.InvariantCulture))
name = name.Substring(0, name.Length - "Model".Length);
return name;
}

/// <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)
.Where(x => x.CanWrite);

/// <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 (or Ignore is specified)

//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
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();

}
}
28 changes: 28 additions & 0 deletions src/GraphQL.DI/DIGraphAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using System.Text;

namespace GraphQL.DI
{
/// <summary>
/// Marks a method's return graph type to be a specified DI graph type.
/// Useful when the return type cannot be inferred (often when it is of type <see cref="object"/>).
/// </summary>
[AttributeUsage(AttributeTargets.Method, Inherited = false)]
public class DIGraphAttribute : Attribute
{
/// <summary>
/// Marks a method's return graph type to be a specified DI graph type.
/// </summary>
/// <param name="graphBaseType">A type that inherits <see cref="DIObjectGraphBase"/>.</param>
public DIGraphAttribute(Type graphBaseType)
{
GraphBaseType = graphBaseType;
}

/// <summary>
/// The DI graph type that inherits <see cref="DIObjectGraphBase"/>.
/// </summary>
public Type GraphBaseType { get; }
}
}
25 changes: 10 additions & 15 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,17 +47,17 @@ 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;
}

/// <summary>
/// This is a required base type of all DI-created graph types. <see cref="DIObjectGraphBase{TSource}"/> may be
/// used when the <see cref="IResolveFieldContext.Source"/> type is not <see cref="object"/>.
/// </summary>
public abstract class DIObjectGraphBase : DIObjectGraphBase<object>, IDIObjectGraphBase<object>
public abstract class DIObjectGraphBase : DIObjectGraphBase<object>
{
}
}
Loading