diff --git a/src/Core/Abstractions/StateAttribute.cs b/src/Core/Abstractions/StateAttribute.cs index c6519d43b4f..9d041a2a99d 100644 --- a/src/Core/Abstractions/StateAttribute.cs +++ b/src/Core/Abstractions/StateAttribute.cs @@ -20,5 +20,9 @@ public StateAttribute(string key) } public string Key { get; } + + public bool IsScoped { get; set; } + + public bool DefaultIfNotExists { get; set; } } } diff --git a/src/Core/Types.Tests/Resolvers/ResolverPropertyGeneratorTests.cs b/src/Core/Types.Tests/Resolvers/ResolverPropertyGeneratorTests.cs index 0158c5da339..eeee7fc68d0 100644 --- a/src/Core/Types.Tests/Resolvers/ResolverPropertyGeneratorTests.cs +++ b/src/Core/Types.Tests/Resolvers/ResolverPropertyGeneratorTests.cs @@ -1,18 +1,14 @@ -using System; +using System.Collections.Immutable; using System.Collections.Generic; +using System; using System.Linq; -using System.Linq.Expressions; using System.Reflection; -using System.Text; using System.Threading; using System.Threading.Tasks; using HotChocolate.Language; -using HotChocolate.Resolvers.CodeGeneration; -using HotChocolate.Resolvers.Expressions.Parameters; using HotChocolate.Subscriptions; using HotChocolate.Types; using Moq; -using Snapshooter.Xunit; using Xunit; namespace HotChocolate.Resolvers.Expressions @@ -738,6 +734,155 @@ public async Task Compile_Arguments_Service() Assert.True(result); } + [Fact] + public async Task Compile_Arguments_ContextData() + { + // arrange + Type type = typeof(Resolvers); + MemberInfo resolverMember = + type.GetMethod("ResolveWithContextData"); + var resolverDescriptor = new ResolverDescriptor( + type, + new FieldMember("A", "b", resolverMember)); + var contextData = new Dictionary + { + { "foo", "bar"} + }; + + // act + var compiler = new ResolverCompiler(); + FieldResolver resolver = compiler.Compile(resolverDescriptor); + + // assert + var context = new Mock(); + context.Setup(t => t.Parent()).Returns(new Resolvers()); + context.Setup(t => t.ContextData).Returns(contextData); + string result = (string)await resolver.Resolver(context.Object); + Assert.Equal("bar", result); + } + + [Fact] + public async Task Compile_Arguments_ContextData_DefaultValue() + { + // arrange + Type type = typeof(Resolvers); + MemberInfo resolverMember = + type.GetMethod("ResolveWithContextDataDefault"); + var resolverDescriptor = new ResolverDescriptor( + type, + new FieldMember("A", "b", resolverMember)); + var contextData = new Dictionary(); + + // act + var compiler = new ResolverCompiler(); + FieldResolver resolver = compiler.Compile(resolverDescriptor); + + // assert + var context = new Mock(); + context.Setup(t => t.Parent()).Returns(new Resolvers()); + context.Setup(t => t.ContextData).Returns(contextData); + object result = await resolver.Resolver(context.Object); + Assert.Null(result); + } + + [Fact] + public void Compile_Arguments_ContextData_NotExists() + { + // arrange + Type type = typeof(Resolvers); + MemberInfo resolverMember = + type.GetMethod("ResolveWithContextData"); + var resolverDescriptor = new ResolverDescriptor( + type, + new FieldMember("A", "b", resolverMember)); + var contextData = new Dictionary(); + + // act + var compiler = new ResolverCompiler(); + FieldResolver resolver = compiler.Compile(resolverDescriptor); + + // assert + var context = new Mock(); + context.Setup(t => t.Parent()).Returns(new Resolvers()); + context.Setup(t => t.ContextData).Returns(contextData); + Action action = () => resolver.Resolver(context.Object); + Assert.Throws(action); + } + + [Fact] + public async Task Compile_Arguments_ScopedContextData() + { + // arrange + Type type = typeof(Resolvers); + MemberInfo resolverMember = + type.GetMethod("ResolveWithScopedContextData"); + var resolverDescriptor = new ResolverDescriptor( + type, + new FieldMember("A", "b", resolverMember)); + var contextData = ImmutableDictionary.Empty + .SetItem("foo", "bar"); + + // act + var compiler = new ResolverCompiler(); + FieldResolver resolver = compiler.Compile(resolverDescriptor); + + // assert + var context = new Mock(); + context.Setup(t => t.Parent()).Returns(new Resolvers()); + context.Setup(t => t.ScopedContextData).Returns(contextData); + string result = (string)await resolver.Resolver(context.Object); + Assert.Equal("bar", result); + } + + [Fact] + public async Task Compile_Arguments_ScopedContextData_DefaultValue() + { + // arrange + Type type = typeof(Resolvers); + MemberInfo resolverMember = + type.GetMethod("ResolveWithScopedContextDataDefault"); + var resolverDescriptor = new ResolverDescriptor( + type, + new FieldMember("A", "b", resolverMember)); + var contextData = ImmutableDictionary.Empty; + + // act + var compiler = new ResolverCompiler(); + FieldResolver resolver = compiler.Compile(resolverDescriptor); + + // assert + var context = new Mock(); + context.Setup(t => t.Parent()).Returns(new Resolvers()); + context.Setup(t => t.ScopedContextData).Returns(contextData); + object result = await resolver.Resolver(context.Object); + Assert.Null(result); + } + + [Fact] + public void Compile_Arguments_ScopedContextData_NotExists() + { + // arrange + Type type = typeof(Resolvers); + MemberInfo resolverMember = + type.GetMethod("ResolveWithScopedContextData"); + var resolverDescriptor = new ResolverDescriptor( + type, + new FieldMember("A", "b", resolverMember)); + var contextData = ImmutableDictionary.Empty; + + // act + var compiler = new ResolverCompiler(); + FieldResolver resolver = compiler.Compile(resolverDescriptor); + + // assert + var context = new Mock(); + context.Setup(t => t.Parent()).Returns(new Resolvers()); + context.Setup(t => t.ScopedContextData).Returns(contextData); + Action action = () => resolver.Resolver(context.Object); + Assert.Throws(action); + } + + public class Resolvers { public Task ObjectTaskResolver() => @@ -806,6 +951,19 @@ public bool ResolverWithSchema( public bool ResolverWithService( [Service]MyService service) => service != null; + + public string ResolveWithContextData( + [State("foo")]string s) => s; + + public string ResolveWithContextDataDefault( + [State("foo", DefaultIfNotExists = true)]string s) => s; + + public string ResolveWithScopedContextData( + [State("foo", IsScoped = true)]string s) => s; + + public string ResolveWithScopedContextDataDefault( + [State("foo", IsScoped = true, DefaultIfNotExists = true)] + string s) => s; } public class Entity { } diff --git a/src/Core/Types/Resolvers/Expressions/Arguments/GetCustomContextCompiler.cs b/src/Core/Types/Resolvers/Expressions/Arguments/GetCustomContextCompiler.cs new file mode 100644 index 00000000000..f9960e72a86 --- /dev/null +++ b/src/Core/Types/Resolvers/Expressions/Arguments/GetCustomContextCompiler.cs @@ -0,0 +1,64 @@ +using System; +using System.Linq.Expressions; +using System.Reflection; + +namespace HotChocolate.Resolvers.Expressions.Parameters +{ + internal sealed class GetCustomContextCompiler + : ResolverParameterCompilerBase + where T : IResolverContext + { + private static readonly MethodInfo _resolveContextData = + typeof(ExpressionHelper).GetMethod("ResolveContextData"); + private static readonly MethodInfo _resolveScopedContextData = + typeof(ExpressionHelper).GetMethod("ResolveScopedContextData"); + + private readonly PropertyInfo _contextData; + private readonly PropertyInfo _scopedContextData; + + public GetCustomContextCompiler() + { + _contextData = typeof(IHasContextData) + .GetTypeInfo().GetDeclaredProperty( + nameof(IResolverContext.ContextData)); + _scopedContextData = ContextTypeInfo.GetDeclaredProperty( + nameof(IResolverContext.ScopedContextData)); + } + + public override bool CanHandle( + ParameterInfo parameter, + Type sourceType) => + parameter.IsDefined(typeof(StateAttribute)); + + public override Expression Compile( + Expression context, + ParameterInfo parameter, + Type sourceType) + { + StateAttribute attribute = + parameter.GetCustomAttribute(); + + ConstantExpression key = + Expression.Constant(attribute.Key); + + ConstantExpression defaultIfNotExists = + Expression.Constant(attribute.DefaultIfNotExists); + + MemberExpression contextData = attribute.IsScoped + ? Expression.Property(context, _scopedContextData) + : Expression.Property(context, _contextData); + + MethodInfo resolveContextData = attribute.IsScoped + ? _resolveScopedContextData.MakeGenericMethod( + parameter.ParameterType) + : _resolveContextData.MakeGenericMethod( + parameter.ParameterType); + + return Expression.Call( + resolveContextData, + contextData, + key, + defaultIfNotExists); + } + } +} diff --git a/src/Core/Types/Resolvers/Expressions/Arguments/ParameterCompilerFactory.cs b/src/Core/Types/Resolvers/Expressions/Arguments/ParameterCompilerFactory.cs index bcf3c311a0c..062a6c86bb5 100644 --- a/src/Core/Types/Resolvers/Expressions/Arguments/ParameterCompilerFactory.cs +++ b/src/Core/Types/Resolvers/Expressions/Arguments/ParameterCompilerFactory.cs @@ -4,7 +4,7 @@ namespace HotChocolate.Resolvers.Expressions.Parameters { internal static class ParameterCompilerFactory { - public static IEnumerable CreateForResolverContext() + public static IEnumerable Create() { return CreateFor(); } @@ -14,6 +14,7 @@ private static IEnumerable CreateFor() { yield return new GetCancellationTokenCompiler(); yield return new GetContextCompiler(); + yield return new GetCustomContextCompiler(); yield return new GetDataLoaderCompiler(); yield return new GetEventMessageCompiler(); yield return new GetFieldSelectionCompiler(); diff --git a/src/Core/Types/Resolvers/Expressions/ResolverCompiler.cs b/src/Core/Types/Resolvers/Expressions/ResolverCompiler.cs index 5abe4c31baf..ca72d63758b 100644 --- a/src/Core/Types/Resolvers/Expressions/ResolverCompiler.cs +++ b/src/Core/Types/Resolvers/Expressions/ResolverCompiler.cs @@ -25,7 +25,7 @@ internal sealed class ResolverCompiler private readonly MethodInfo _taskResult; public ResolverCompiler() - : this(ParameterCompilerFactory.CreateForResolverContext()) + : this(ParameterCompilerFactory.Create()) { } @@ -185,5 +185,59 @@ public static Task WrapResultHelper(T result) { return Task.FromResult(result); } + + public static TContextData ResolveContextData( + IDictionary contextData, + string key, + bool defaultIfNotExists) + { + if (contextData.TryGetValue(key, out object value)) + { + if (value is null) + { + return default; + } + + if (value is TContextData v) + { + return v; + } + } + else if (defaultIfNotExists) + { + return default; + } + + // TODO : resources + throw new ArgumentException( + "The specified context key does not exist."); + } + + public static TContextData ResolveScopedContextData( + IReadOnlyDictionary contextData, + string key, + bool defaultIfNotExists) + { + if (contextData.TryGetValue(key, out object value)) + { + if (value is null) + { + return default; + } + + if (value is TContextData v) + { + return v; + } + } + else if (defaultIfNotExists) + { + return default; + } + + // TODO : resources + throw new ArgumentException( + "The specified context key does not exist."); + } } }