-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[resolvers]: Delegates as resolvers (#1584)
+ Add Delegate as resolver + Delegate can take ResolverContext argument + Delegate can take any property of ResolverContext as argument (by name, ignores case) + Delegate can take additional arguments which are resolved from ResolverContext.RequestServices + Delegate return value can be void, ValueTask, Task + Delegate return value can be T, ValueTask<T>, Task<T> and the returned value is assigned to ResolverContext.ResolvedValue automatically
Showing
24 changed files
with
825 additions
and
61 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
47 changes: 47 additions & 0 deletions
47
src/GraphQL/Executable/ObjectDelegateResolversConfiguration.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
using Tanka.GraphQL.Language.Nodes.TypeSystem; | ||
using Tanka.GraphQL.ValueResolution; | ||
|
||
namespace Tanka.GraphQL.Executable; | ||
|
||
public class ObjectDelegateResolversConfiguration : IExecutableSchemaConfiguration | ||
{ | ||
public ObjectDelegateResolversConfiguration( | ||
string type, | ||
IReadOnlyDictionary<FieldDefinition, Delegate> fields) | ||
{ | ||
Type = type; | ||
Fields = fields; | ||
} | ||
|
||
public string Type { get; } | ||
|
||
public IReadOnlyDictionary<FieldDefinition, Delegate> Fields { get; } | ||
|
||
public Task Configure(SchemaBuilder schema, ResolversBuilder resolvers) | ||
{ | ||
return Task.WhenAll(Configure(schema), Configure(resolvers)); | ||
} | ||
|
||
private Task Configure(ResolversBuilder builder) | ||
{ | ||
foreach (var (field, resolverDelegate) in Fields) | ||
builder.Resolver(Type, field.Name.Value).Run(resolverDelegate); | ||
|
||
return Task.CompletedTask; | ||
} | ||
|
||
private Task Configure(SchemaBuilder builder) | ||
{ | ||
var fields = Fields.Select(kv => kv.Key).ToList(); | ||
|
||
// we add as type extension so we don't hit any conflicts with type names | ||
// WARNING: if schema is built with BuildTypesFromOrphanedExtensions set to false | ||
// the build will fail | ||
builder.Add(new TypeExtension(new ObjectDefinition( | ||
null, | ||
Type, | ||
fields: new(fields)))); | ||
|
||
return Task.CompletedTask; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,178 @@ | ||
using System.Collections.Concurrent; | ||
using System.Diagnostics; | ||
using System.Linq.Expressions; | ||
using System.Reflection; | ||
using Microsoft.Extensions.DependencyInjection; | ||
|
||
namespace Tanka.GraphQL.ValueResolution; | ||
|
||
public static class DelegateResolverFactory | ||
{ | ||
private static readonly ParameterExpression ContextParam = Expression.Parameter(typeof(ResolverContext), "context"); | ||
|
||
private static readonly IReadOnlyDictionary<string, Expression> ContextParamProperties = typeof(ResolverContext) | ||
.GetProperties(BindingFlags.Public | BindingFlags.Instance) | ||
.ToDictionary(p => p.Name.ToLowerInvariant(), p => (Expression)Expression.Property(ContextParam, p)); | ||
|
||
|
||
private static readonly ConcurrentDictionary<Delegate, Resolver> Cache = new ConcurrentDictionary<Delegate, Resolver>(); | ||
|
||
public static Resolver GetOrCreate(Delegate resolverDelegate) | ||
{ | ||
if (Cache.TryGetValue(resolverDelegate, out var resolver)) | ||
return resolver; | ||
|
||
return Create(resolverDelegate); | ||
} | ||
|
||
public static Resolver Create(Delegate resolverDelegate) | ||
{ | ||
#if DEBUG | ||
Trace.WriteLine( | ||
$"Available context parameters:\n {string.Join(',', ContextParamProperties.Select(p => string.Concat($"{p.Key}: {p.Value.Type}")))}"); | ||
#endif | ||
|
||
MethodInfo invokeMethod = resolverDelegate.Method; | ||
|
||
Expression instanceExpression = null; | ||
if (!invokeMethod.IsStatic) instanceExpression = Expression.Constant(resolverDelegate.Target); | ||
|
||
IEnumerable<Expression> argumentsExpressions = invokeMethod.GetParameters() | ||
.Select(p => | ||
{ | ||
if (p.ParameterType == typeof(ResolverContext)) return ContextParam; | ||
|
||
if (p.Name is not null) | ||
if (ContextParamProperties.TryGetValue(p.Name.ToLowerInvariant(), | ||
out Expression? propertyExpression)) | ||
{ | ||
if (p.ParameterType == propertyExpression.Type) | ||
return propertyExpression; | ||
|
||
return Expression.Convert(propertyExpression, p.ParameterType); | ||
} | ||
|
||
MemberExpression serviceProviderProperty = Expression.Property(ContextParam, "RequestServices"); | ||
MethodInfo? getServiceMethodInfo = typeof(ServiceProviderServiceExtensions) | ||
.GetMethods() | ||
.FirstOrDefault(m => m.Name == nameof(ServiceProviderServiceExtensions.GetRequiredService) | ||
&& m.GetParameters().Length == 1 | ||
&& m.GetParameters()[0].ParameterType == typeof(IServiceProvider) | ||
&& m.IsGenericMethodDefinition); | ||
|
||
if (getServiceMethodInfo is null) | ||
throw new InvalidOperationException("Could not find GetRequiredService method"); | ||
|
||
Type serviceType = p.ParameterType; | ||
MethodInfo genericMethodInfo = getServiceMethodInfo.MakeGenericMethod(serviceType); | ||
var getRequiredServiceCall = (Expression)Expression.Call( | ||
null, | ||
genericMethodInfo, | ||
serviceProviderProperty); | ||
|
||
return Expression.Block( | ||
getRequiredServiceCall | ||
); | ||
}); | ||
|
||
MethodCallExpression invokeExpression = Expression.Call( | ||
instanceExpression, | ||
invokeMethod, | ||
argumentsExpressions | ||
); | ||
|
||
Expression valueTaskExpression; | ||
if (invokeMethod.ReturnType == typeof(ValueTask)) | ||
{ | ||
valueTaskExpression = invokeExpression; | ||
} | ||
else if (invokeMethod.ReturnType == typeof(Task)) | ||
{ | ||
valueTaskExpression = Expression.New( | ||
typeof(ValueTask).GetConstructor(new[] { typeof(Task) })!, | ||
invokeExpression | ||
); | ||
} | ||
else if (invokeMethod.ReturnType == typeof(void)) | ||
{ | ||
valueTaskExpression = Expression.Block( | ||
invokeExpression, | ||
Expression.Constant(ValueTask.CompletedTask) | ||
); | ||
} | ||
else if (invokeMethod.ReturnType.IsGenericType && | ||
invokeMethod.ReturnType.GetGenericTypeDefinition() == typeof(Task<>)) | ||
{ | ||
var t = invokeMethod.ReturnType.GetGenericArguments()[0]; | ||
valueTaskExpression = Expression.Call(ResolveValueTaskMethod.MakeGenericMethod(t), invokeExpression, ContextParam); | ||
} | ||
else if (invokeMethod.ReturnType.IsGenericType && | ||
invokeMethod.ReturnType.GetGenericTypeDefinition() == typeof(ValueTask<>)) | ||
{ | ||
var t = invokeMethod.ReturnType.GetGenericArguments()[0]; | ||
valueTaskExpression = Expression.Call(ResolveValueValueTaskMethod.MakeGenericMethod(t), invokeExpression, ContextParam); | ||
} | ||
else | ||
{ | ||
var t = invokeMethod.ReturnType; | ||
valueTaskExpression = Expression.Call(ResolveValueObjectMethod.MakeGenericMethod(t), invokeExpression, ContextParam); | ||
} | ||
|
||
|
||
var lambda = Expression.Lambda<Resolver>( | ||
valueTaskExpression, | ||
ContextParam | ||
); | ||
|
||
var compiledLambda = lambda.Compile(); | ||
Cache.TryAdd(resolverDelegate, compiledLambda); | ||
return compiledLambda; | ||
} | ||
|
||
private static readonly MethodInfo ResolveValueTaskMethod = typeof(DelegateResolverFactory) | ||
.GetMethod(nameof(ResolveValueTask), BindingFlags.Static | BindingFlags.NonPublic)!; | ||
|
||
private static readonly MethodInfo ResolveValueValueTaskMethod = typeof(DelegateResolverFactory) | ||
.GetMethod(nameof(ResolveValueValueTask), BindingFlags.Static | BindingFlags.NonPublic)!; | ||
|
||
private static readonly MethodInfo ResolveValueObjectMethod = typeof(DelegateResolverFactory) | ||
.GetMethod(nameof(ResolveValueObject), BindingFlags.Static | BindingFlags.NonPublic)!; | ||
|
||
private static ValueTask ResolveValueTask<T>(Task<T> task, ResolverContext context) | ||
{ | ||
static async ValueTask AwaitResolveValue(Task<T> task, ResolverContext context) | ||
{ | ||
context.ResolvedValue = await task; | ||
} | ||
|
||
if (task.IsCompletedSuccessfully) | ||
{ | ||
context.ResolvedValue = task.Result; | ||
return default; | ||
} | ||
|
||
return AwaitResolveValue(task, context); | ||
} | ||
|
||
private static ValueTask ResolveValueValueTask<T>(ValueTask<T> task, ResolverContext context) | ||
{ | ||
static async ValueTask AwaitResolveValue(ValueTask<T> task, ResolverContext context) | ||
{ | ||
context.ResolvedValue = await task; | ||
} | ||
|
||
if (task.IsCompletedSuccessfully) | ||
{ | ||
context.ResolvedValue = task.Result; | ||
return default; | ||
} | ||
|
||
return AwaitResolveValue(task, context); | ||
} | ||
|
||
private static ValueTask ResolveValueObject<T>(T result, ResolverContext context) | ||
{ | ||
context.ResolvedValue = result; | ||
return ValueTask.CompletedTask; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
466 changes: 466 additions & 0 deletions
466
tests/graphql.tests/ValueResolution/DelegateResolverFactoryFacts.cs
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
namespace Tanka.GraphQL.Tests.ValueResolution; | ||
|
||
public interface IMyDependency | ||
{ | ||
} | ||
|
||
class MyDependency : IMyDependency | ||
{ | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters