diff --git a/README.md b/README.md index 153456ce3bd..a6f256ed731 100644 --- a/README.md +++ b/README.md @@ -269,8 +269,8 @@ Moreover, we are working on the following parts that are not defined in the spec ### Execution Engine -- [ ] Custom Context Objects -- [ ] Data Loader Integration / Batched Operations +- [x] Custom Context Objects +- [x] Data Loader Integration / Batched Operations ### Schema Creation diff --git a/run-tests.ps1 b/run-tests.ps1 index e511220e21e..8fd37425714 100644 --- a/run-tests.ps1 +++ b/run-tests.ps1 @@ -1,4 +1,5 @@ dotnet build src +dotnet test src/Runtime.Tests dotnet test src/Language.Tests dotnet test src/Core.Tests dotnet test src/AspNetCore.Tests diff --git a/run-tests.sh b/run-tests.sh index 110155a7bd1..a35cc9087cf 100644 --- a/run-tests.sh +++ b/run-tests.sh @@ -1,4 +1,5 @@ dotnet build src +dotnet test src/Runtime.Tests dotnet test src/Language.Tests --no-build dotnet test src/Core.Tests --no-build dotnet test src/AspNetCore.Tests --no-build diff --git a/src/Abstractions/DataLoaderAttribute.cs b/src/Abstractions/DataLoaderAttribute.cs new file mode 100644 index 00000000000..37f5c830ac0 --- /dev/null +++ b/src/Abstractions/DataLoaderAttribute.cs @@ -0,0 +1,10 @@ +using System; + +namespace HotChocolate +{ + [AttributeUsage(AttributeTargets.Parameter)] + public sealed class DataLoaderAttribute + : Attribute + { + } +} diff --git a/src/Abstractions/IResolverResult.cs b/src/Abstractions/IResolverResult.cs new file mode 100644 index 00000000000..486ad1a6c88 --- /dev/null +++ b/src/Abstractions/IResolverResult.cs @@ -0,0 +1,17 @@ +namespace HotChocolate +{ + public interface IResolverResult + { + string ErrorMessage { get; } + + bool IsError { get; } + + object Value { get; } + } + + public interface IResolverResult + : IResolverResult + { + new TValue Value { get; } + } +} diff --git a/src/Abstractions/ResolverResult.cs b/src/Abstractions/ResolverResult.cs new file mode 100644 index 00000000000..c264a83cfec --- /dev/null +++ b/src/Abstractions/ResolverResult.cs @@ -0,0 +1,31 @@ +using System; + +namespace HotChocolate +{ + public readonly struct ResolverResult + : IResolverResult + { + public ResolverResult(string errorMessage) + { + Value = default; + ErrorMessage = errorMessage + ?? throw new ArgumentNullException(nameof(errorMessage)); + IsError = true; + } + + public ResolverResult(TValue value) + { + Value = default; + ErrorMessage = null; + IsError = false; + } + + public TValue Value { get; } + + public string ErrorMessage { get; } + + public bool IsError { get; } + + object IResolverResult.Value => Value; + } +} diff --git a/src/AspNetCore.Tests/QueryMiddlewareTests.cs b/src/AspNetCore.Tests/QueryMiddlewareTests.cs index 54f4710392c..ea46b919b53 100644 --- a/src/AspNetCore.Tests/QueryMiddlewareTests.cs +++ b/src/AspNetCore.Tests/QueryMiddlewareTests.cs @@ -202,6 +202,8 @@ public async Task HttpPost_WithHttpContext() Query = @" { requestPath + requestPath2 + requestPath3 }" }; diff --git a/src/AspNetCore.Tests/Schema/Query.cs b/src/AspNetCore.Tests/Schema/Query.cs index 0f3d2e8c5d4..27dc6792b5c 100644 --- a/src/AspNetCore.Tests/Schema/Query.cs +++ b/src/AspNetCore.Tests/Schema/Query.cs @@ -1,4 +1,5 @@ using System; +using HotChocolate.Resolvers; using Microsoft.AspNetCore.Http; namespace HotChocolate.AspNetCore @@ -26,6 +27,16 @@ public string GetRequestPath() return _context.Request.Path; } + public string GetRequestPath2(IResolverContext context) + { + return context.Service().Request.Path; + } + + public string GetRequestPath3([Service]HttpContext context) + { + return context.Request.Path; + } + public Foo GetBasic() { return new Foo diff --git a/src/AspNetCore.Tests/__snapshots__/HttpPost_WithHttpContext.json b/src/AspNetCore.Tests/__snapshots__/HttpPost_WithHttpContext.json index 27c53616672..aef137eb67b 100644 --- a/src/AspNetCore.Tests/__snapshots__/HttpPost_WithHttpContext.json +++ b/src/AspNetCore.Tests/__snapshots__/HttpPost_WithHttpContext.json @@ -1,6 +1,8 @@ { "Data": { - "requestPath": "/" + "requestPath": "/", + "requestPath2": "/", + "requestPath3": "/" }, "Errors": null } diff --git a/src/Core.Tests/Execution/ArgumentTests.cs b/src/Core.Tests/Execution/ArgumentTests.cs index 8aa4d9d39e1..5ef81a5cf4b 100644 --- a/src/Core.Tests/Execution/ArgumentTests.cs +++ b/src/Core.Tests/Execution/ArgumentTests.cs @@ -76,7 +76,7 @@ public async Task ListOfFoo() // act IExecutionResult result = await schema.ExecuteAsync( - "query x($x:[Foo]) { a(foo:$x) { foo } }", + "query x($x:[FooInput]) { a(foo:$x) { foo } }", new Dictionary { { "x", list } }); // assert @@ -140,7 +140,7 @@ public async Task SingleFoo() // act IExecutionResult result = await schema.ExecuteAsync( - "query x($x:Foo) { a(foo:$x) { foo } }", + "query x($x:FooInput) { a(foo:$x) { foo } }", new Dictionary { { "x", obj } }); // assert diff --git a/src/Core.Tests/Execution/OperationExecuterErrorTests.cs b/src/Core.Tests/Execution/OperationExecuterErrorTests.cs index 88bf87194b9..2d0f3682ba5 100644 --- a/src/Core.Tests/Execution/OperationExecuterErrorTests.cs +++ b/src/Core.Tests/Execution/OperationExecuterErrorTests.cs @@ -186,6 +186,7 @@ private Schema CreateSchema() return Schema.Create(c => { + c.Options.ExecutionTimeout = TimeSpan.FromSeconds(30); c.Options.StrictValidation = true; c.RegisterQueryType(); }); diff --git a/src/Core.Tests/Execution/OperationRequestTests.cs b/src/Core.Tests/Execution/OperationRequestTests.cs index 2c38078ee66..84cac7088e7 100644 --- a/src/Core.Tests/Execution/OperationRequestTests.cs +++ b/src/Core.Tests/Execution/OperationRequestTests.cs @@ -4,6 +4,7 @@ using System.Threading.Tasks; using HotChocolate.Language; using HotChocolate.Runtime; +using Moq; using Xunit; namespace HotChocolate.Execution @@ -15,11 +16,9 @@ public async Task ResolveSimpleOneLevelQuery() { // arrange Schema schema = CreateSchema(); - var dataLoaderDescriptors = - new DataLoaderDescriptorCollection(schema.DataLoaders); - var dataLoaderState = new DataLoaderState( - schema.Services, dataLoaderDescriptors, - Enumerable.Empty>()); + var session = new Mock(MockBehavior.Strict); + session.Setup(t => t.DataLoaders).Returns((IDataLoaderProvider)null); + session.Setup(t => t.CustomContexts).Returns((ICustomContextProvider)null); DocumentNode query = Parser.Default.Parse(@" { a @@ -31,7 +30,7 @@ public async Task ResolveSimpleOneLevelQuery() OperationExecuter operationExecuter = new OperationExecuter(schema, query, operation); IExecutionResult result = await operationExecuter.ExecuteAsync( - new OperationRequest(schema.Services, dataLoaderState), + new OperationRequest(schema.Services, session.Object), CancellationToken.None); // assert @@ -60,11 +59,10 @@ public async Task ExecuteMutationSerially() cnf.BindResolver(ctx => state = ctx.Argument("newNumber")) .To("Mutation", "changeTheNumber"); }); - var dataLoaderDescriptors = - new DataLoaderDescriptorCollection(schema.DataLoaders); - var dataLoaderState = new DataLoaderState( - schema.Services, dataLoaderDescriptors, - Enumerable.Empty>()); + + var session = new Mock(MockBehavior.Strict); + session.Setup(t => t.DataLoaders).Returns((IDataLoaderProvider)null); + session.Setup(t => t.CustomContexts).Returns((ICustomContextProvider)null); DocumentNode query = Parser.Default.Parse( FileResource.Open("MutationExecutionQuery.graphql")); @@ -75,7 +73,7 @@ public async Task ExecuteMutationSerially() OperationExecuter operationExecuter = new OperationExecuter(schema, query, operation); IExecutionResult result = await operationExecuter.ExecuteAsync( - new OperationRequest(schema.Services, dataLoaderState), + new OperationRequest(schema.Services, session.Object), CancellationToken.None); // assert diff --git a/src/Core.Tests/Integration/DataLoader/CustomContextTests.cs b/src/Core.Tests/Integration/DataLoader/CustomContextTests.cs new file mode 100644 index 00000000000..793bbdf3ca9 --- /dev/null +++ b/src/Core.Tests/Integration/DataLoader/CustomContextTests.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using HotChocolate.Execution; +using HotChocolate.Runtime; +using Xunit; + +namespace HotChocolate.Integration.DataLoader +{ + public class CustomContextTests + { + [Fact] + public async Task RequestCustomContext() + { + // arrange + ISchema schema = CreateSchema(ExecutionScope.Request); + QueryExecuter executer = new QueryExecuter(schema, 10); + + // act + List results = new List(); + results.Add(await executer.ExecuteAsync( + new QueryRequest("{ a: a b: a }"))); + results.Add(await executer.ExecuteAsync( + new QueryRequest("{ a: a b: a }"))); + results.Add(await executer.ExecuteAsync( + new QueryRequest("{ a: a b: a }"))); + results.Add(await executer.ExecuteAsync( + new QueryRequest("{ a: a b: a }"))); + + // assert + Assert.Collection(results, + t => Assert.Null(t.Errors), + t => Assert.Null(t.Errors), + t => Assert.Null(t.Errors), + t => Assert.Null(t.Errors)); + Assert.Equal(Snapshot.Current(), Snapshot.New(results)); + } + + [Fact] + public async Task GlobalCustomContext() + { + // arrange + ISchema schema = CreateSchema(ExecutionScope.Global); + QueryExecuter executer = new QueryExecuter(schema, 10); + + // act + List results = new List(); + results.Add(await executer.ExecuteAsync(new QueryRequest("{ a }"))); + results.Add(await executer.ExecuteAsync(new QueryRequest("{ a }"))); + results.Add(await executer.ExecuteAsync(new QueryRequest("{ a }"))); + results.Add(await executer.ExecuteAsync(new QueryRequest("{ a }"))); + + // assert + Assert.Collection(results, + t => Assert.Null(t.Errors), + t => Assert.Null(t.Errors), + t => Assert.Null(t.Errors), + t => Assert.Null(t.Errors)); + Assert.Equal(Snapshot.Current(), Snapshot.New(results)); + } + + private static ISchema CreateSchema(ExecutionScope scope) + { + return Schema.Create("type Query { a: String }", c => + { + c.Options.ExecutionTimeout = TimeSpan.FromSeconds(30); + c.RegisterCustomContext(scope); + c.BindResolver(ctx => + { + MyCustomContext cctx = ctx.CustomContext(); + cctx.Count = cctx.Count + 1; + return cctx.Count.ToString(); + }).To("Query", "a"); + }); + } + } +} diff --git a/src/Core.Tests/Integration/DataLoader/DataLoaderTests.cs b/src/Core.Tests/Integration/DataLoader/DataLoaderTests.cs new file mode 100644 index 00000000000..f200dfff2a3 --- /dev/null +++ b/src/Core.Tests/Integration/DataLoader/DataLoaderTests.cs @@ -0,0 +1,90 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using HotChocolate.Execution; +using HotChocolate.Runtime; +using Xunit; + +namespace HotChocolate.Integration.DataLoader +{ + public class DataLoaderTests + { + [Fact] + public async Task RequestDataLoader() + { + // arrange + ISchema schema = CreateSchema(ExecutionScope.Request); + QueryExecuter executer = new QueryExecuter(schema, 10); + + // act + List results = new List(); + results.Add(await executer.ExecuteAsync(new QueryRequest( + @"{ + a: withDataLoader(key: ""a"") + b: withDataLoader(key: ""b"") + }"))); + results.Add(await executer.ExecuteAsync(new QueryRequest( + @"{ + a: withDataLoader(key: ""a"") + }"))); + results.Add(await executer.ExecuteAsync(new QueryRequest( + @"{ + c: withDataLoader(key: ""c"") + }"))); + results.Add(await executer.ExecuteAsync(new QueryRequest( + "{ loads }"))); + + // assert + Assert.Collection(results, + t => Assert.Null(t.Errors), + t => Assert.Null(t.Errors), + t => Assert.Null(t.Errors), + t => Assert.Null(t.Errors)); + Assert.Equal(Snapshot.Current(), Snapshot.New(results)); + } + + [Fact] + public async Task GlobalDataLoader() + { + // arrange + ISchema schema = CreateSchema(ExecutionScope.Global); + QueryExecuter executer = new QueryExecuter(schema, 10); + + // act + List results = new List(); + results.Add(await executer.ExecuteAsync(new QueryRequest( + @"{ + a: withDataLoader(key: ""a"") + b: withDataLoader(key: ""b"") + }"))); + results.Add(await executer.ExecuteAsync(new QueryRequest( + @"{ + a: withDataLoader(key: ""a"") + }"))); + results.Add(await executer.ExecuteAsync(new QueryRequest( + @"{ + c: withDataLoader(key: ""c"") + }"))); + results.Add(await executer.ExecuteAsync(new QueryRequest( + "{ loads }"))); + + // assert + Assert.Collection(results, + t => Assert.Null(t.Errors), + t => Assert.Null(t.Errors), + t => Assert.Null(t.Errors), + t => Assert.Null(t.Errors)); + Assert.Equal(Snapshot.Current(), Snapshot.New(results)); + } + + private static ISchema CreateSchema(ExecutionScope scope) + { + return Schema.Create(c => + { + c.Options.ExecutionTimeout = TimeSpan.FromSeconds(30); + c.RegisterDataLoader(scope); + c.RegisterQueryType(); + }); + } + } +} diff --git a/src/Core.Tests/Integration/DataLoader/MyCustomContext.cs b/src/Core.Tests/Integration/DataLoader/MyCustomContext.cs new file mode 100644 index 00000000000..3cf1c55a081 --- /dev/null +++ b/src/Core.Tests/Integration/DataLoader/MyCustomContext.cs @@ -0,0 +1,7 @@ +namespace HotChocolate.Integration.DataLoader +{ + public class MyCustomContext + { + public int Count { get; set; } + } +} diff --git a/src/Core.Tests/Integration/DataLoader/Query.cs b/src/Core.Tests/Integration/DataLoader/Query.cs new file mode 100644 index 00000000000..e36711793c6 --- /dev/null +++ b/src/Core.Tests/Integration/DataLoader/Query.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using HotChocolate.Language; + +namespace HotChocolate.Integration.DataLoader +{ + public class Query + { + public Task GetWithDataLoader( + string key, + FieldNode fieldSelection, + [DataLoader]TestDataLoader testDataLoader) + { + return testDataLoader.LoadAsync(key); + } + + public List GetLoads([DataLoader]TestDataLoader testDataLoader) + { + List list = new List(); + + foreach (IReadOnlyList request in testDataLoader.Loads) + { + list.Add(string.Join(", ", request)); + } + + return list; + } + } +} diff --git a/src/Core.Tests/Integration/DataLoader/TestDataLoader.cs b/src/Core.Tests/Integration/DataLoader/TestDataLoader.cs new file mode 100644 index 00000000000..48a38539bf7 --- /dev/null +++ b/src/Core.Tests/Integration/DataLoader/TestDataLoader.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using GreenDonut; + +namespace HotChocolate.Integration.DataLoader +{ + public class TestDataLoader + : DataLoaderBase + { + public TestDataLoader() + : base(new DataLoaderOptions()) + { + } + + public List> Loads { get; } = + new List>(); + + protected override Task>> Fetch( + IReadOnlyList keys) + { + Loads.Add(keys.OrderBy(t => t).ToArray()); + return Task.FromResult>>( + keys.Select(t => Result.Resolve(t)).ToArray()); + } + } +} diff --git a/src/Core.Tests/Integration/InputOutputObjectAreTheSame/InputOutputObjectAreTheSame.cs b/src/Core.Tests/Integration/InputOutputObjectAreTheSame/InputOutputObjectAreTheSame.cs index 94a79e19b19..01db127eed2 100644 --- a/src/Core.Tests/Integration/InputOutputObjectAreTheSame/InputOutputObjectAreTheSame.cs +++ b/src/Core.Tests/Integration/InputOutputObjectAreTheSame/InputOutputObjectAreTheSame.cs @@ -31,7 +31,7 @@ public void ExecuteQueryThatReturnsPerson() // act IExecutionResult result = schema.Execute(@"{ - person(person: { firstName:""a"", lastName:""b"" }) { + person(person: { firstName:""a"", lastName:""b"" }) { lastName firstName } diff --git a/src/Core.Tests/Integration/StarWarsCodeFirst/StarWarsCodeFirstTests.cs b/src/Core.Tests/Integration/StarWarsCodeFirst/StarWarsCodeFirstTests.cs index 8773212003a..09a1abdf680 100644 --- a/src/Core.Tests/Integration/StarWarsCodeFirst/StarWarsCodeFirstTests.cs +++ b/src/Core.Tests/Integration/StarWarsCodeFirst/StarWarsCodeFirstTests.cs @@ -107,9 +107,9 @@ public void GraphQLOrgFragmentExample() rightComparison: hero(episode: JEDI) { ...comparisonFields } - } + } - fragment comparisonFields on Character { + fragment comparisonFields on Character { name appearsIn friends { diff --git a/src/Core.Tests/Internal/DotNetTypeInfoFactoryTests.cs b/src/Core.Tests/Internal/DotNetTypeInfoFactoryTests.cs index b91ec5937ac..c5b376f5712 100644 --- a/src/Core.Tests/Internal/DotNetTypeInfoFactoryTests.cs +++ b/src/Core.Tests/Internal/DotNetTypeInfoFactoryTests.cs @@ -10,6 +10,15 @@ namespace HotChocolate.Internal public class DotNetTypeInfoFactoryTests { [InlineData(typeof(string), "String")] + [InlineData(typeof(IResolverResult), "String")] + [InlineData(typeof(IResolverResult), "[String]")] + [InlineData(typeof(IResolverResult>), "[String]")] + [InlineData(typeof(Task>), "String")] + [InlineData(typeof(Task>), "[String]")] + [InlineData(typeof(Task>>), "[String]")] + [InlineData(typeof(ResolverResult), "String")] + [InlineData(typeof(ResolverResult), "[String]")] + [InlineData(typeof(ResolverResult>), "[String]")] [InlineData(typeof(Task), "String")] [InlineData(typeof(List), "[String]")] [InlineData(typeof(Task>), "[String]")] diff --git a/src/Core.Tests/Validation/AllVariableUsagesAreAllowedRuleTests.cs b/src/Core.Tests/Validation/AllVariableUsagesAreAllowedRuleTests.cs index 271e47822ca..a24c65a2105 100644 --- a/src/Core.Tests/Validation/AllVariableUsagesAreAllowedRuleTests.cs +++ b/src/Core.Tests/Validation/AllVariableUsagesAreAllowedRuleTests.cs @@ -33,7 +33,8 @@ query intCannotGoIntoBoolean($intArg: Int) { t => Assert.Equal( "The variable `intArg` type is not " + "compatible with the type of the " + - "argument `booleanArg`.", + "argument `booleanArg`." + + "\r\nExpected type: `Boolean`.", t.Message)); } @@ -59,7 +60,8 @@ query booleanListCannotGoIntoBoolean($booleanListArg: [Boolean]) { t => Assert.Equal( "The variable `booleanListArg` type is not " + "compatible with the type of the " + - "argument `booleanArg`.", + "argument `booleanArg`." + + "\r\nExpected type: `Boolean`.", t.Message)); } @@ -85,7 +87,8 @@ query booleanArgQuery($booleanArg: Boolean) { t => Assert.Equal( "The variable `booleanArg` type is not " + "compatible with the type of the " + - "argument `nonNullBooleanArg`.", + "argument `nonNullBooleanArg`." + + "\r\nExpected type: `Boolean`.", t.Message)); } @@ -131,7 +134,8 @@ query listToNonNullList($booleanList: [Boolean]) { t => Assert.Equal( "The variable `booleanList` type is not " + "compatible with the type of the " + - "argument `nonNullBooleanListArg`.", + "argument `nonNullBooleanListArg`." + + "\r\nExpected type: `Boolean`.", t.Message)); } diff --git a/src/Core.Tests/__snapshots__/ExecuteQueryThatReturnsPerson.json b/src/Core.Tests/__snapshots__/ExecuteQueryThatReturnsPerson.json new file mode 100644 index 00000000000..7d56549e9b4 --- /dev/null +++ b/src/Core.Tests/__snapshots__/ExecuteQueryThatReturnsPerson.json @@ -0,0 +1,9 @@ +{ + "Data": { + "person": { + "lastName": "b", + "firstName": "a" + } + }, + "Errors": null +} diff --git a/src/Core.Tests/__snapshots__/GlobalCustomContext.json b/src/Core.Tests/__snapshots__/GlobalCustomContext.json new file mode 100644 index 00000000000..c38f0117bee --- /dev/null +++ b/src/Core.Tests/__snapshots__/GlobalCustomContext.json @@ -0,0 +1,26 @@ +[ + { + "Data": { + "a": "1" + }, + "Errors": null + }, + { + "Data": { + "a": "2" + }, + "Errors": null + }, + { + "Data": { + "a": "3" + }, + "Errors": null + }, + { + "Data": { + "a": "4" + }, + "Errors": null + } +] diff --git a/src/Core.Tests/__snapshots__/GlobalDataLoader.json b/src/Core.Tests/__snapshots__/GlobalDataLoader.json new file mode 100644 index 00000000000..9d194d88ef0 --- /dev/null +++ b/src/Core.Tests/__snapshots__/GlobalDataLoader.json @@ -0,0 +1,30 @@ +[ + { + "Data": { + "a": "a", + "b": "b" + }, + "Errors": null + }, + { + "Data": { + "a": "a" + }, + "Errors": null + }, + { + "Data": { + "c": "c" + }, + "Errors": null + }, + { + "Data": { + "loads": [ + "a, b", + "c" + ] + }, + "Errors": null + } +] diff --git a/src/Core.Tests/__snapshots__/RequestCustomContext.json b/src/Core.Tests/__snapshots__/RequestCustomContext.json new file mode 100644 index 00000000000..96cc3a3c841 --- /dev/null +++ b/src/Core.Tests/__snapshots__/RequestCustomContext.json @@ -0,0 +1,30 @@ +[ + { + "Data": { + "a": "1", + "b": "2" + }, + "Errors": null + }, + { + "Data": { + "a": "1", + "b": "2" + }, + "Errors": null + }, + { + "Data": { + "a": "1", + "b": "2" + }, + "Errors": null + }, + { + "Data": { + "a": "1", + "b": "2" + }, + "Errors": null + } +] diff --git a/src/Core.Tests/__snapshots__/RequestDataLoader.json b/src/Core.Tests/__snapshots__/RequestDataLoader.json new file mode 100644 index 00000000000..759a22b23a8 --- /dev/null +++ b/src/Core.Tests/__snapshots__/RequestDataLoader.json @@ -0,0 +1,27 @@ +[ + { + "Data": { + "a": "a", + "b": "b" + }, + "Errors": null + }, + { + "Data": { + "a": "a" + }, + "Errors": null + }, + { + "Data": { + "c": "c" + }, + "Errors": null + }, + { + "Data": { + "loads": [] + }, + "Errors": null + } +] diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj index 970a75c5cb3..721b10631c0 100644 --- a/src/Core/Core.csproj +++ b/src/Core/Core.csproj @@ -22,6 +22,10 @@ true + + + + diff --git a/src/Core/DataLoaderConfigurationExtensions.cs b/src/Core/DataLoaderConfigurationExtensions.cs new file mode 100644 index 00000000000..c0ca492aad0 --- /dev/null +++ b/src/Core/DataLoaderConfigurationExtensions.cs @@ -0,0 +1,72 @@ +using System; +using GreenDonut; +using HotChocolate.Runtime; + +namespace HotChocolate +{ + public static class DataLoaderConfigurationExtensions + { + public static void RegisterDataLoader( + this ISchemaConfiguration configuration, + string key, + ExecutionScope scope, + Func loaderFactory) + where TLoader : IDispatchableDataLoader + { + configuration.RegisterDataLoader( + key, scope, loaderFactory, + (d, c) => d.DispatchAsync()); + } + + public static void RegisterDataLoader( + this ISchemaConfiguration configuration, + ExecutionScope scope, + Func loaderFactory) + where TLoader : IDispatchableDataLoader + { + RegisterDataLoader( + configuration, typeof(TLoader).FullName, + scope, loaderFactory); + } + + public static void RegisterDataLoader( + this ISchemaConfiguration configuration, + Func loaderFactory) + where TLoader : IDispatchableDataLoader + { + RegisterDataLoader( + configuration, typeof(TLoader).FullName, + ExecutionScope.Request, loaderFactory); + } + + public static void RegisterDataLoader( + this ISchemaConfiguration configuration, + string key, + ExecutionScope scope) + where TLoader : class, IDispatchableDataLoader + { + configuration.RegisterDataLoader( + key, scope, triggerLoaderAsync: + (d, c) => d.DispatchAsync()); + } + + public static void RegisterDataLoader( + this ISchemaConfiguration configuration, + ExecutionScope scope) + where TLoader : class, IDispatchableDataLoader + { + RegisterDataLoader( + configuration, typeof(TLoader).FullName, + scope); + } + + public static void RegisterDataLoader( + this ISchemaConfiguration configuration) + where TLoader : class, IDispatchableDataLoader + { + RegisterDataLoader( + configuration, typeof(TLoader).FullName, + ExecutionScope.Request); + } + } +} diff --git a/src/Core/Execution/Errors/Location.cs b/src/Core/Execution/Errors/Location.cs index 0a23d21a383..085eafba414 100644 --- a/src/Core/Execution/Errors/Location.cs +++ b/src/Core/Execution/Errors/Location.cs @@ -2,7 +2,7 @@ namespace HotChocolate.Execution { - public class Location + public readonly struct Location { public Location(int line, int column) { diff --git a/src/Core/Execution/ExecutionContext.cs b/src/Core/Execution/ExecutionContext.cs index f12fcb81fcf..fe7816a958f 100644 --- a/src/Core/Execution/ExecutionContext.cs +++ b/src/Core/Execution/ExecutionContext.cs @@ -12,6 +12,9 @@ internal class ExecutionContext { private readonly List _errors = new List(); private readonly FieldCollector _fieldCollector; + private readonly ISession _session; + private readonly bool _disposeRootValue; + private bool _disposed; public ExecutionContext( ISchema schema, @@ -27,8 +30,10 @@ public ExecutionContext( Schema = schema ?? throw new ArgumentNullException(nameof(schema)); + Services = request.Services; - DataLoaders = request.DataLoaders; + _session = request.Session; + QueryDocument = queryDocument ?? throw new ArgumentNullException(nameof(queryDocument)); Operation = operation @@ -41,6 +46,11 @@ public ExecutionContext( OperationType = schema.GetOperationType(operation.Operation); RootValue = ResolveRootValue(request.Services, schema, OperationType, request.InitialValue); + if (RootValue == null) + { + RootValue = CreateRootValue(Services, schema, OperationType); + _disposeRootValue = true; + } } public ISchema Schema { get; } @@ -61,7 +71,9 @@ public ExecutionContext( public VariableCollection Variables { get; } - public IDataLoaderState DataLoaders { get; } + public IDataLoaderProvider DataLoaders => _session.DataLoaders; + + public ICustomContextProvider CustomContexts => _session.CustomContexts; public IReadOnlyCollection CollectFields( ObjectType objectType, SelectionSetNode selectionSet) @@ -104,19 +116,43 @@ private static object ResolveRootValue( if (initialValue == null && schema.TryGetNativeType( operationType.Name, out Type nativeType)) { - initialValue = services.GetService(nativeType) - ?? CreateRootValue(services, nativeType); + initialValue = services.GetService(nativeType); } return initialValue; } private static object CreateRootValue( IServiceProvider services, - Type nativeType) + ISchema schema, + ObjectType operationType) + { + if (schema.TryGetNativeType( + operationType.Name, + out Type nativeType)) + { + ServiceFactory serviceFactory = new ServiceFactory(); + serviceFactory.Services = services; + return serviceFactory.CreateInstance(nativeType); + } + return null; + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) { - ServiceFactory serviceFactory = new ServiceFactory(); - serviceFactory.Services = services; - return serviceFactory.CreateInstance(nativeType); + if (!_disposed && disposing) + { + if (_disposeRootValue && RootValue is IDisposable d) + { + d.Dispose(); + } + _disposed = true; + } } } } diff --git a/src/Core/Execution/ExecutionStrategyBase.Helpers.cs b/src/Core/Execution/ExecutionStrategyBase.Helpers.cs index 60aaad9d614..037f9503054 100644 --- a/src/Core/Execution/ExecutionStrategyBase.Helpers.cs +++ b/src/Core/Execution/ExecutionStrategyBase.Helpers.cs @@ -42,6 +42,16 @@ protected static async Task FinalizeResolverResultAsync( case Task task: return await FinalizeResolverResultTaskAsync( fieldSelection, task, isDeveloperMode); + + case IResolverResult result: + if (result.IsError) + { + return new FieldError( + result.ErrorMessage, + fieldSelection); + } + return result.Value; + default: return resolverResult; } diff --git a/src/Core/Execution/ExecutionStrategyBase.cs b/src/Core/Execution/ExecutionStrategyBase.cs index 371806b65f2..11aa68dfae0 100644 --- a/src/Core/Execution/ExecutionStrategyBase.cs +++ b/src/Core/Execution/ExecutionStrategyBase.cs @@ -4,6 +4,7 @@ using System.Threading; using System.Threading.Tasks; using HotChocolate.Resolvers; +using HotChocolate.Runtime; namespace HotChocolate.Execution { @@ -48,7 +49,9 @@ private async Task ExecuteResolverBatchAsync( executionContext, currentBatch, cancellationToken); // execute batch data loaders - await CompleteDataLoadersAsync(executionContext, cancellationToken); + await CompleteDataLoadersAsync( + executionContext.DataLoaders, + cancellationToken); // await field resolver results await EndExecuteResolverBatchAsync( @@ -62,7 +65,8 @@ private void BeginExecuteResolverBatch( { foreach (ResolverTask resolverTask in currentBatch) { - if (resolverTask.Path.Depth <= executionContext.Options.MaxExecutionDepth) + if (resolverTask.Path.Depth <= executionContext + .Options.MaxExecutionDepth) { resolverTask.ResolverResult = ExecuteResolver( resolverTask, executionContext.Options.DeveloperMode, @@ -71,7 +75,8 @@ private void BeginExecuteResolverBatch( else { executionContext.ReportError(resolverTask.CreateError( - $"The field has a depth of {resolverTask.Path.Depth}, " + + "The field has a depth of " + + $"{resolverTask.Path.Depth}, " + "which exceeds max allowed depth of " + $"{executionContext.Options.MaxExecutionDepth}")); } @@ -81,13 +86,15 @@ private void BeginExecuteResolverBatch( } protected async Task CompleteDataLoadersAsync( - IExecutionContext executionContext, + IDataLoaderProvider dataLoaders, CancellationToken cancellationToken) { - await Task.WhenAll(executionContext - .DataLoaders.Touched - .Select(t => t.TriggerAsync(cancellationToken))); - executionContext.DataLoaders.Reset(); + if (dataLoaders != null) + { + await Task.WhenAll(dataLoaders.Touched + .Select(t => t.TriggerAsync(cancellationToken))); + dataLoaders.Reset(); + } } private async Task EndExecuteResolverBatchAsync( diff --git a/src/Core/Execution/FieldSelection.cs b/src/Core/Execution/FieldSelection.cs index 381576a0515..db3588a957f 100644 --- a/src/Core/Execution/FieldSelection.cs +++ b/src/Core/Execution/FieldSelection.cs @@ -6,7 +6,7 @@ namespace HotChocolate.Execution { [DebuggerDisplay("{Field.Name}: {Field.Type}")] - public class FieldSelection + internal class FieldSelection { public FieldSelection(FieldNode node, ObjectField field, string responseName) { diff --git a/src/Core/Execution/IExecutionContext.cs b/src/Core/Execution/IExecutionContext.cs index a2a87e4e7de..bf5712cc619 100644 --- a/src/Core/Execution/IExecutionContext.cs +++ b/src/Core/Execution/IExecutionContext.cs @@ -8,6 +8,7 @@ namespace HotChocolate.Execution { internal interface IExecutionContext + : IDisposable { // schema ISchema Schema { get; } @@ -16,7 +17,8 @@ internal interface IExecutionContext // context object RootValue { get; } - IDataLoaderState DataLoaders { get; } + IDataLoaderProvider DataLoaders { get; } + ICustomContextProvider CustomContexts { get; } // query ast DocumentNode QueryDocument { get; } diff --git a/src/Core/Execution/IExecutionResult.cs b/src/Core/Execution/IExecutionResult.cs new file mode 100644 index 00000000000..31b04bdf4f4 --- /dev/null +++ b/src/Core/Execution/IExecutionResult.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace HotChocolate.Execution +{ + public interface IExecutionResult + { + IReadOnlyCollection Errors { get; } + } +} diff --git a/src/Core/Execution/IOrderedDictionary.cs b/src/Core/Execution/IOrderedDictionary.cs new file mode 100644 index 00000000000..555bc8ef181 --- /dev/null +++ b/src/Core/Execution/IOrderedDictionary.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; + +namespace HotChocolate.Execution +{ + public interface IOrderedDictionary + : IReadOnlyDictionary + { + + } +} diff --git a/src/Core/Execution/IQueryExecutionResult.cs b/src/Core/Execution/IQueryExecutionResult.cs new file mode 100644 index 00000000000..98ffba3f506 --- /dev/null +++ b/src/Core/Execution/IQueryExecutionResult.cs @@ -0,0 +1,12 @@ +namespace HotChocolate.Execution +{ + public interface IQueryExecutionResult + : IExecutionResult + { + IOrderedDictionary Data { get; } + + T ToObject(); + + string ToJson(); + } +} diff --git a/src/Core/Execution/IQueryResult.cs b/src/Core/Execution/IQueryResult.cs deleted file mode 100644 index 1452711df14..00000000000 --- a/src/Core/Execution/IQueryResult.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace HotChocolate.Execution -{ - public interface IExecutionResult - { - IReadOnlyCollection Errors { get; } - } - - public interface IQueryExecutionResult - : IExecutionResult - { - IOrderedDictionary Data { get; } - - T ToObject(); - - string ToJson(); - } - - public interface IOrderedDictionary - : IReadOnlyDictionary - { - - } - - public interface ISubscriptionExecutionResult - : IExecutionResult - , IObservable - { - - } -} diff --git a/src/Core/Execution/ISubscriptionExecutionResult.cs b/src/Core/Execution/ISubscriptionExecutionResult.cs new file mode 100644 index 00000000000..2968632369b --- /dev/null +++ b/src/Core/Execution/ISubscriptionExecutionResult.cs @@ -0,0 +1,11 @@ +using System; + +namespace HotChocolate.Execution +{ + public interface ISubscriptionExecutionResult + : IExecutionResult + , IObservable + { + + } +} diff --git a/src/Core/Execution/MutationExecutionStrategy.cs b/src/Core/Execution/MutationExecutionStrategy.cs index 7e5edc8965a..692b2c7f877 100644 --- a/src/Core/Execution/MutationExecutionStrategy.cs +++ b/src/Core/Execution/MutationExecutionStrategy.cs @@ -70,7 +70,9 @@ private async Task ExecuteResolverSeriallyAsync( resolverTask, executionContext.Options.DeveloperMode, cancellationToken); - await CompleteDataLoadersAsync(executionContext, cancellationToken); + await CompleteDataLoadersAsync( + executionContext.DataLoaders, + cancellationToken); // await async results resolverTask.ResolverResult = await FinalizeResolverResultAsync( diff --git a/src/Core/Execution/OperationExecuter.cs b/src/Core/Execution/OperationExecuter.cs index 39fb3fa864e..3e8e44293ba 100644 --- a/src/Core/Execution/OperationExecuter.cs +++ b/src/Core/Execution/OperationExecuter.cs @@ -62,16 +62,17 @@ public async Task ExecuteAsync( CancellationTokenSource.CreateLinkedTokenSource( requestTimeoutCts.Token, cancellationToken); + IExecutionContext executionContext = + CreateExecutionContext(request); + try { - IExecutionContext executionContext = - CreateExecutionContext(request); - return await _strategy.ExecuteAsync( executionContext, combinedCts.Token); } finally { + executionContext.Dispose(); combinedCts.Dispose(); requestTimeoutCts.Dispose(); } diff --git a/src/Core/Execution/OperationRequest.cs b/src/Core/Execution/OperationRequest.cs index f2c68b1d45e..408f8ae25d8 100644 --- a/src/Core/Execution/OperationRequest.cs +++ b/src/Core/Execution/OperationRequest.cs @@ -5,20 +5,20 @@ namespace HotChocolate.Execution { - public class OperationRequest + internal class OperationRequest { public OperationRequest( IServiceProvider services, - IDataLoaderState dataLoaders) + ISession session) { Services = services ?? throw new ArgumentNullException(nameof(services)); - DataLoaders = dataLoaders - ?? throw new ArgumentNullException(nameof(dataLoaders)); + Session = session + ?? throw new ArgumentNullException(nameof(session)); } public IServiceProvider Services { get; } - public IDataLoaderState DataLoaders { get; } + public ISession Session { get; } public IReadOnlyDictionary VariableValues { get; set; } public object InitialValue { get; set; } } diff --git a/src/Core/Execution/QueryExecuter.cs b/src/Core/Execution/QueryExecuter.cs index 7469e0dd850..e210d394a73 100644 --- a/src/Core/Execution/QueryExecuter.cs +++ b/src/Core/Execution/QueryExecuter.cs @@ -3,6 +3,7 @@ using System.Threading.Tasks; using HotChocolate.Validation; using HotChocolate.Runtime; +using System.Linq; namespace HotChocolate.Execution { @@ -12,7 +13,6 @@ public partial class QueryExecuter private readonly QueryValidator _queryValidator; private readonly Cache _queryCache; private readonly Cache _operationCache; - private readonly DataLoaderStateManager _dataLoaderStateManager; private readonly bool _useCache; public QueryExecuter(ISchema schema) @@ -26,8 +26,6 @@ public QueryExecuter(ISchema schema, int cacheSize) _queryValidator = new QueryValidator(schema); _queryCache = new Cache(cacheSize); _operationCache = new Cache(cacheSize * 10); - _dataLoaderStateManager = new DataLoaderStateManager( - schema.DataLoaders, cacheSize); _useCache = cacheSize > 0; CacheSize = cacheSize; } @@ -53,13 +51,14 @@ public async Task ExecuteAsync( return new QueryResult(queryInfo.ValidationResult.Errors); } + OperationRequest operationRequest = null; try { OperationExecuter operationExecuter = GetOrCreateOperationExecuter( queryRequest, queryInfo.QueryDocument); - OperationRequest operationRequest = + operationRequest = CreateOperationRequest(queryRequest); return await operationExecuter.ExecuteAsync( @@ -73,6 +72,10 @@ public async Task ExecuteAsync( { return new QueryResult(CreateErrorFromException(ex)); } + finally + { + operationRequest?.Session.Dispose(); + } } private IQueryError CreateErrorFromException(Exception exception) @@ -93,14 +96,18 @@ private OperationRequest CreateOperationRequest( { IServiceProvider services = queryRequest.Services ?? _schema.Services; - DataLoaderState dataLoaderState = _dataLoaderStateManager - .CreateState(services, "anonymous"); - return new OperationRequest(services, dataLoaderState) + return new OperationRequest(services, + _schema.Sessions.CreateSession(services)) { VariableValues = queryRequest.VariableValues, - InitialValue = queryRequest.InitialValue + InitialValue = queryRequest.InitialValue, }; } + + private string CreateUserKey(QueryRequest queryRequest) + { + return queryRequest.UserKey ?? "none"; + } } } diff --git a/src/Core/Execution/QueryRequest.cs b/src/Core/Execution/QueryRequest.cs index 2b5df23b679..196336079fb 100644 --- a/src/Core/Execution/QueryRequest.cs +++ b/src/Core/Execution/QueryRequest.cs @@ -35,5 +35,7 @@ public QueryRequest(string query, string operationName) public object InitialValue { get; set; } public IServiceProvider Services { get; set; } + + public string UserKey { get; set; } } } diff --git a/src/Core/Execution/ResolverContext.cs b/src/Core/Execution/ResolverContext.cs index 3bc8840c0e5..da4469dfe28 100644 --- a/src/Core/Execution/ResolverContext.cs +++ b/src/Core/Execution/ResolverContext.cs @@ -13,7 +13,7 @@ namespace HotChocolate.Execution internal readonly struct ResolverContext : IResolverContext { - // remove + // todo: remove private static readonly List _converters = new List { @@ -86,10 +86,9 @@ private T ConvertArgumentValue(string name, ArgumentValue argumentValue) return value; } - Type type = typeof(T); if (argumentValue.Value == null) { - return default(T); + return default; } if (TryConvertValue(argumentValue, out value)) @@ -99,10 +98,10 @@ private T ConvertArgumentValue(string name, ArgumentValue argumentValue) throw new QueryException( new FieldError( - $"Could not convert argument {name} from " + - $"{argumentValue.NativeType.FullName} to " + - $"{typeof(T).FullName}.", - _resolverTask.FieldSelection.Node)); + $"Could not convert argument {name} from " + + $"{argumentValue.NativeType.FullName} to " + + $"{typeof(T).FullName}.", + _resolverTask.FieldSelection.Node)); } private bool TryConvertValue(ArgumentValue argumentValue, out T value) @@ -132,13 +131,21 @@ public T Service() return (T)_executionContext.Services.GetService(typeof(T)); } - public T State() + public T CustomContext() { - throw new NotImplementedException(); + if (_executionContext.CustomContexts == null) + { + return default; + } + return _executionContext.CustomContexts.GetCustomContext(); } - public T Loader(string key) + public T DataLoader(string key) { + if (_executionContext.DataLoaders == null) + { + return default; + } return _executionContext.DataLoaders.GetDataLoader(key); } } diff --git a/src/Core/Validation/AllVariableUsagesAreAllowedVisitor.cs b/src/Core/Validation/AllVariableUsagesAreAllowedVisitor.cs index b2d89adfad5..eeea4672d8b 100644 --- a/src/Core/Validation/AllVariableUsagesAreAllowedVisitor.cs +++ b/src/Core/Validation/AllVariableUsagesAreAllowedVisitor.cs @@ -104,7 +104,8 @@ private void FindVariableUsageErrors() Errors.Add(new ValidationError( $"The variable `{variableName}` type is not " + "compatible with the type of the argument " + - $"`{variableUsage.InputField.Name}`.", + $"`{variableUsage.InputField.Name}`.\r\n" + + $"Expected type: `{variableUsage.Type.TypeName()}`.", variableUsage.Argument, variableDefinition)); } } diff --git a/src/Core/Validation/AllVariablesUsedVisitor.cs b/src/Core/Validation/AllVariablesUsedVisitor.cs index d4bc62a0722..186f21675fc 100644 --- a/src/Core/Validation/AllVariablesUsedVisitor.cs +++ b/src/Core/Validation/AllVariablesUsedVisitor.cs @@ -1,7 +1,6 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; -using HotChocolate.Internal; using HotChocolate.Language; using HotChocolate.Types; @@ -17,7 +16,9 @@ public AllVariablesUsedVisitor(ISchema schema) { } - public override void VisitDocument(DocumentNode document) + protected override void VisitDocument( + DocumentNode document, + ImmutableStack path) { HashSet declaredVariables = new HashSet(); @@ -33,7 +34,7 @@ public override void VisitDocument(DocumentNode document) } VisitOperationDefinition(operation, - ImmutableStack.Empty.Push(document)); + path.Push(document)); declaredVariables.ExceptWith(_usedVariables); if (declaredVariables.Count > 0) diff --git a/src/Core/Validation/FieldSelectionMergingVisitor.cs b/src/Core/Validation/FieldSelectionMergingVisitor.cs index 6e73b129e37..7291c66101d 100644 --- a/src/Core/Validation/FieldSelectionMergingVisitor.cs +++ b/src/Core/Validation/FieldSelectionMergingVisitor.cs @@ -21,11 +21,10 @@ public FieldSelectionMergingVisitor(ISchema schema) { } - public override void VisitDocument(DocumentNode document) + protected override void VisitDocument( + DocumentNode document, + ImmutableStack path) { - ImmutableStack path = - ImmutableStack.Empty.Push(document); - foreach (OperationDefinitionNode operation in document.Definitions .OfType()) { @@ -47,7 +46,7 @@ public override void VisitDocument(DocumentNode document) protected override void VisitField( FieldNode field, - Types.IType type, + IType type, ImmutableStack path) { if (TryGetSelectionSet(path, out SelectionSetNode selectionSet) @@ -65,7 +64,10 @@ protected override void VisitSelectionSet( IType type, ImmutableStack path) { - _fieldSelectionSets.Add(selectionSet, new List()); + if (!_fieldSelectionSets.ContainsKey(selectionSet)) + { + _fieldSelectionSets.Add(selectionSet, new List()); + } base.VisitSelectionSet(selectionSet, type, path); } @@ -277,7 +279,7 @@ private IType GetType(FieldInfo fieldInfo) private readonly struct FieldInfo { - public FieldInfo(Types.IType declaringType, FieldNode field) + public FieldInfo(IType declaringType, FieldNode field) { DeclaringType = declaringType ?? throw new ArgumentNullException(nameof(declaringType)); @@ -289,7 +291,7 @@ public FieldInfo(Types.IType declaringType, FieldNode field) } public string ResponseName { get; } - public Types.IType DeclaringType { get; } + public IType DeclaringType { get; } public FieldNode Field { get; } } } diff --git a/src/Core/Validation/QueryValidator.cs b/src/Core/Validation/QueryValidator.cs index fb9b00b58ad..9006ba5de9e 100644 --- a/src/Core/Validation/QueryValidator.cs +++ b/src/Core/Validation/QueryValidator.cs @@ -9,7 +9,6 @@ namespace HotChocolate.Validation public class QueryValidator { private static readonly IQueryValidationRule[] _rules = - new IQueryValidationRule[] { new ExecutableDefinitionsRule(), new LoneAnonymousOperationRule(), diff --git a/src/Core/Validation/QueryVisitor.cs b/src/Core/Validation/QueryVisitor.cs index c09efec2bb4..92b17cd9e71 100644 --- a/src/Core/Validation/QueryVisitor.cs +++ b/src/Core/Validation/QueryVisitor.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; -using HotChocolate.Internal; using HotChocolate.Language; using HotChocolate.Types; @@ -10,6 +9,8 @@ namespace HotChocolate.Validation { internal class QueryVisitor { + private readonly Dictionary _fragments = + new Dictionary(); private readonly HashSet _visitedFragments = new HashSet(); @@ -20,17 +21,35 @@ protected QueryVisitor(ISchema schema) protected ISchema Schema { get; } - public virtual void VisitDocument(DocumentNode document) + public void VisitDocument(DocumentNode document) { ImmutableStack path = ImmutableStack.Empty.Push(document); + // create fragment definition set. + foreach (FragmentDefinitionNode fragment in document.Definitions + .OfType()) + { + if (!_fragments.ContainsKey(fragment.Name.Value)) + { + _fragments[fragment.Name.Value] = fragment; + } + } + + VisitDocument(document, path); + } + + + protected virtual void VisitDocument( + DocumentNode document, + ImmutableStack path) + { VisitOperationDefinitions( document.Definitions.OfType(), path); VisitFragmentDefinitions( - document.Definitions.OfType(), + _fragments.Values, path); } @@ -62,6 +81,7 @@ protected virtual void VisitOperationDefinition( ImmutableStack newPath = path.Push(operation); VisitSelectionSet(operation.SelectionSet, operationType, newPath); VisitDirectives(operation.Directives, newPath); + _visitedFragments.Clear(); } protected virtual void VisitSelectionSet( @@ -106,7 +126,7 @@ protected virtual void VisitField( if (field.SelectionSet != null) { VisitSelectionSet(field.SelectionSet, - complexType.Fields[field.Name.Value].Type, + complexType.Fields[field.Name.Value].Type.NamedType(), newpath); } } @@ -124,16 +144,11 @@ protected virtual void VisitFragmentSpread( if (path.Last() is DocumentNode d) { string fragmentName = fragmentSpread.Name.Value; - if (_visitedFragments.Add(fragmentName)) + if (_visitedFragments.Add(fragmentName) + && _fragments.TryGetValue(fragmentName, + out FragmentDefinitionNode fragment)) { - IEnumerable fragments = d.Definitions - .OfType() - .Where(t => t.Name.Value.EqualsOrdinal(fragmentName)); - - foreach (FragmentDefinitionNode fragment in fragments) - { - VisitFragmentDefinition(fragment, newpath); - } + VisitFragmentDefinition(fragment, newpath); } } @@ -146,7 +161,7 @@ protected virtual void VisitInlineFragment( ImmutableStack path) { if (inlineFragment.TypeCondition?.Name?.Value != null - && Schema.TryGetType( + && Schema.TryGetType( inlineFragment.TypeCondition.Name.Value, out INamedOutputType typeCondition)) { @@ -168,7 +183,7 @@ protected virtual void VisitFragmentDefinition( ImmutableStack path) { if (fragmentDefinition.TypeCondition?.Name?.Value != null - && Schema.TryGetType( + && Schema.TryGetType( fragmentDefinition.TypeCondition.Name.Value, out INamedOutputType typeCondition)) { @@ -202,5 +217,10 @@ protected virtual void VisitDirective( { } + + protected void ClearVisitedFragments() + { + _visitedFragments.Clear(); + } } } diff --git a/src/Core/Validation/SubscriptionSingleRootFieldVisitor.cs b/src/Core/Validation/SubscriptionSingleRootFieldVisitor.cs index 5fb1d89148c..13173aeb6b6 100644 --- a/src/Core/Validation/SubscriptionSingleRootFieldVisitor.cs +++ b/src/Core/Validation/SubscriptionSingleRootFieldVisitor.cs @@ -17,7 +17,9 @@ public SubscriptionSingleRootFieldVisitor(ISchema schema) { } - public override void VisitDocument(DocumentNode document) + protected override void VisitDocument( + DocumentNode document, + ImmutableStack path) { foreach (OperationDefinitionNode operation in document.Definitions .OfType() @@ -26,7 +28,7 @@ public override void VisitDocument(DocumentNode document) _fieldCount = 0; VisitOperationDefinition(operation, - ImmutableStack.Empty.Push(document)); + path.Push(document)); if (_fieldCount > 1) { diff --git a/src/Language/Language.csproj b/src/Language/Language.csproj index 26947488ba3..09b5fc756b8 100644 --- a/src/Language/Language.csproj +++ b/src/Language/Language.csproj @@ -26,10 +26,4 @@ - - - - - - diff --git a/src/Runtime.Tests/CacheEntryEventArgsTests.cs b/src/Runtime.Tests/CacheEntryEventArgsTests.cs new file mode 100644 index 00000000000..763ee693195 --- /dev/null +++ b/src/Runtime.Tests/CacheEntryEventArgsTests.cs @@ -0,0 +1,40 @@ +using System; +using Xunit; + +namespace HotChocolate.Runtime +{ + public class CacheEntryEventArgsTests + { + [Fact] + public void ValueIsNull() + { + // act + var eventArgs = new CacheEntryEventArgs("key", null); + + // assert + Assert.Equal("key", eventArgs.Key); + Assert.Null(eventArgs.Value); + } + + [Fact] + public void ValueAndKeyAreSet() + { + // act + var eventArgs = new CacheEntryEventArgs("key", "value"); + + // assert + Assert.Equal("key", eventArgs.Key); + Assert.Equal("value", eventArgs.Value); + } + + [Fact] + public void KeyIsNull() + { + // act + Action action = () => new CacheEntryEventArgs(null, "value"); + + // assert + Assert.Throws(action); + } + } +} diff --git a/src/Runtime.Tests/CacheTests.cs b/src/Runtime.Tests/CacheTests.cs new file mode 100644 index 00000000000..2217ef0abfc --- /dev/null +++ b/src/Runtime.Tests/CacheTests.cs @@ -0,0 +1,29 @@ +using Xunit; + +namespace HotChocolate.Runtime +{ + public class CacheTests + { + [Fact] + public void CacheItemRemoved() + { + // arrange + string removedValue = null; + Cache cache = new Cache(10); + cache.RemovedEntry += (s, e) => { removedValue = e.Value; }; + for (int i = 0; i < 10; i++) + { + cache.GetOrCreate(i.ToString(), () => i.ToString()); + } + + // assert + string value = cache.GetOrCreate("10", () => "10"); + + // assert + Assert.Equal("10", value); + Assert.Equal("0", removedValue); + Assert.Equal(10, cache.Size); + Assert.Equal(10, cache.Usage); + } + } +} diff --git a/src/Runtime.Tests/CustomContextDescriptorTests.cs b/src/Runtime.Tests/CustomContextDescriptorTests.cs new file mode 100644 index 00000000000..89251956a43 --- /dev/null +++ b/src/Runtime.Tests/CustomContextDescriptorTests.cs @@ -0,0 +1,47 @@ +using System; +using Xunit; + +namespace HotChocolate.Runtime +{ + public class CustomContextDescriptorTests + { + [Fact] + public void CreateCustomContextDescriptor() + { + // act + var descriptor = new CustomContextDescriptor( + typeof(string), sp => "foo", ExecutionScope.Global); + + // assert + Assert.Equal(typeof(string), descriptor.Key); + Assert.Equal(typeof(string), descriptor.Type); + Assert.Equal("foo", descriptor.Factory(null)); + Assert.Equal(ExecutionScope.Global, descriptor.Scope); + } + + [Fact] + public void CreateCustomContextDescriptorFactoryIsNull() + { + // act + var descriptor = new CustomContextDescriptor( + typeof(string), null, ExecutionScope.Global); + + // assert + Assert.Equal(typeof(string), descriptor.Key); + Assert.Equal(typeof(string), descriptor.Type); + Assert.Null(descriptor.Factory); + Assert.Equal(ExecutionScope.Global, descriptor.Scope); + } + + [Fact] + public void CreateCustomContextDescriptorTypeIsNull() + { + // act + Action action = () => new CustomContextDescriptor( + null, sp => "foo", ExecutionScope.Global); + + // assert + Assert.Throws(action); + } + } +} diff --git a/src/Runtime.Tests/DataLoaderDescriptorTests.cs b/src/Runtime.Tests/DataLoaderDescriptorTests.cs new file mode 100644 index 00000000000..ea854971516 --- /dev/null +++ b/src/Runtime.Tests/DataLoaderDescriptorTests.cs @@ -0,0 +1,81 @@ +using System; +using Xunit; + +namespace HotChocolate.Runtime +{ + public class DataLoaderDescriptorTests + { + [Fact] + public void CreateDataLoaderDescriptor() + { + // act + var descriptor = new DataLoaderDescriptor( + "123", typeof(string), ExecutionScope.Global, + sp => "foo", (d, c) => null); + + // assert + Assert.Equal("123", descriptor.Key); + Assert.Equal(typeof(string), descriptor.Type); + Assert.Equal(ExecutionScope.Global, descriptor.Scope); + Assert.Equal("foo", descriptor.Factory(null)); + Assert.NotNull(descriptor.TriggerLoadAsync); + } + + [Fact] + public void CreateDataLoaderDescriptorFactoryIsNull() + { + // act + var descriptor = new DataLoaderDescriptor( + "123", typeof(string), ExecutionScope.Global, + null, (d, c) => null); + + // assert + Assert.Equal("123", descriptor.Key); + Assert.Equal(typeof(string), descriptor.Type); + Assert.Equal(ExecutionScope.Global, descriptor.Scope); + Assert.Null(descriptor.Factory); + Assert.NotNull(descriptor.TriggerLoadAsync); + } + + [Fact] + public void CreateDataLoaderDescriptorTriggerIsNull() + { + // act + var descriptor = new DataLoaderDescriptor( + "123", typeof(string), ExecutionScope.Global, + sp => "foo", null); + + // assert + Assert.Equal("123", descriptor.Key); + Assert.Equal(typeof(string), descriptor.Type); + Assert.Equal(ExecutionScope.Global, descriptor.Scope); + Assert.Equal("foo", descriptor.Factory(null)); + Assert.Null(descriptor.TriggerLoadAsync); + } + + [Fact] + public void CreateDataLoaderDescriptorKeyIsNull() + { + // act + Action action = () => new DataLoaderDescriptor( + null, typeof(string), ExecutionScope.Global, + sp => "foo", null); + + // assert + Assert.Throws(action); + } + + [Fact] + public void CreateDataLoaderDescriptorTypeIsNull() + { + // act + Action action = () => new DataLoaderDescriptor( + "123", null, ExecutionScope.Global, + sp => "foo", null); + + // assert + Assert.Throws(action); + } + + } +} diff --git a/src/Runtime/Cache.cs b/src/Runtime/Cache.cs index 2489918d6f1..4bfdb439cc0 100644 --- a/src/Runtime/Cache.cs +++ b/src/Runtime/Cache.cs @@ -13,6 +13,8 @@ public class Cache ImmutableDictionary.Empty; private LinkedListNode _first; + public event EventHandler> RemovedEntry; + public Cache(int size) { Size = size < 10 ? 10 : size; @@ -72,9 +74,13 @@ private void ClearSpaceForNewEntry() { if (_cache.Count >= Size) { - LinkedListNode entry = _ranking.Last; - _cache = _cache.Remove(entry.Value); - _ranking.Remove(entry); + LinkedListNode rank = _ranking.Last; + CacheEntry entry = _cache[rank.Value]; + _cache = _cache.Remove(rank.Value); + _ranking.Remove(rank); + + RemovedEntry?.Invoke(this, + new CacheEntryEventArgs(entry.Key, entry.Value)); } } diff --git a/src/Runtime/CacheEntryEventArgs.cs b/src/Runtime/CacheEntryEventArgs.cs new file mode 100644 index 00000000000..1c688c2819d --- /dev/null +++ b/src/Runtime/CacheEntryEventArgs.cs @@ -0,0 +1,35 @@ +using System; + +namespace HotChocolate.Runtime +{ + /// + /// Represents cache entry event args. + /// + public sealed class CacheEntryEventArgs + : EventArgs + { + /// + /// Initializes a new instance of the + /// class. + /// + /// The cache entry key. + /// The cache entry value. + internal CacheEntryEventArgs(string key, TValue value) + { + Key = key ?? throw new ArgumentNullException(nameof(key)); + Value = value; + } + + /// + /// Gets the cache entry key. + /// + /// The key. + public string Key { get; } + + /// + /// Gets the cache entry value. + /// + /// The value. + public TValue Value { get; } + } +} diff --git a/src/Runtime/StateObjectDescriptor.cs b/src/Runtime/CustomContextDescriptor.cs similarity index 72% rename from src/Runtime/StateObjectDescriptor.cs rename to src/Runtime/CustomContextDescriptor.cs index 4bba2727fd5..269017c3931 100644 --- a/src/Runtime/StateObjectDescriptor.cs +++ b/src/Runtime/CustomContextDescriptor.cs @@ -1,19 +1,18 @@ -using System; +using System; namespace HotChocolate.Runtime { - public class StateObjectDescriptor + public class CustomContextDescriptor : IScopedStateDescriptor { - public StateObjectDescriptor( + public CustomContextDescriptor( Type type, Func factory, ExecutionScope scope) { Type = type ?? throw new ArgumentNullException(nameof(type)); - Factory = factory - ?? throw new ArgumentNullException(nameof(factory)); + Factory = factory; Scope = scope; } diff --git a/src/Runtime/CustomContextProvider.cs b/src/Runtime/CustomContextProvider.cs new file mode 100644 index 00000000000..2c2bf7c872f --- /dev/null +++ b/src/Runtime/CustomContextProvider.cs @@ -0,0 +1,20 @@ +using System; + +namespace HotChocolate.Runtime +{ + public class CustomContextProvider + : StateObjectContainer + , ICustomContextProvider + { + public CustomContextProvider( + IServiceProvider globalServices, + IServiceProvider requestServices, + StateObjectDescriptorCollection descriptors, + StateObjectCollection globalStates) + : base(globalServices, requestServices, descriptors, globalStates) + { + } + + public T GetCustomContext() => (T)GetStateObject(typeof(T)); + } +} diff --git a/src/Runtime/DataLoaderState.cs b/src/Runtime/DataLoaderProvider.cs similarity index 68% rename from src/Runtime/DataLoaderState.cs rename to src/Runtime/DataLoaderProvider.cs index 4df8847173b..e6d67656685 100644 --- a/src/Runtime/DataLoaderState.cs +++ b/src/Runtime/DataLoaderProvider.cs @@ -1,30 +1,23 @@ -using System; +using System; using System.Collections.Generic; using System.Collections.Immutable; namespace HotChocolate.Runtime { - public class DataLoaderState + public class DataLoaderProvider : StateObjectContainer - , IDataLoaderState + , IDataLoaderProvider { - private static readonly HashSet _scopes = - new HashSet - { - ExecutionScope.Global, - ExecutionScope.User, - ExecutionScope.Request - }; - private readonly object _sync = new object(); private ImmutableDictionary _touchedDataLoaders = ImmutableDictionary.Empty; - public DataLoaderState( - IServiceProvider root, - DataLoaderDescriptorCollection descriptors, - IEnumerable> objectCollections) - : base(root, descriptors, _scopes, objectCollections) + public DataLoaderProvider( + IServiceProvider globalServices, + IServiceProvider requestServices, + StateObjectDescriptorCollection descriptors, + StateObjectCollection globalStates) + : base(globalServices, requestServices, descriptors, globalStates) { } @@ -42,7 +35,8 @@ public void Reset() { lock (_sync) { - _touchedDataLoaders = _touchedDataLoaders.Clear(); + _touchedDataLoaders = + ImmutableDictionary.Empty; } } diff --git a/src/Runtime/DataLoaderStateManager.cs b/src/Runtime/DataLoaderStateManager.cs deleted file mode 100644 index 06fe18513e5..00000000000 --- a/src/Runtime/DataLoaderStateManager.cs +++ /dev/null @@ -1,67 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace HotChocolate.Runtime -{ - public class DataLoaderStateManager - { - private readonly StateObjectCollection _globalStateObjects; - private readonly Cache> _cache; - private readonly DataLoaderDescriptorCollection _dataLoaderDescriptors; - - public DataLoaderStateManager( - IEnumerable descriptors, - int size) - { - if (descriptors == null) - { - throw new ArgumentNullException(nameof(descriptors)); - } - - _dataLoaderDescriptors = - new DataLoaderDescriptorCollection(descriptors); - _cache = new Cache>( - size < 10 ? 10 : size); - _globalStateObjects = - new StateObjectCollection(ExecutionScope.Global); - } - - - public DataLoaderState CreateState( - IServiceProvider services, string userKey) - { - if (services == null) - { - throw new ArgumentNullException(nameof(services)); - } - - if (userKey == null) - { - throw new ArgumentNullException(nameof(userKey)); - } - - StateObjectCollection userStateObjects = - GetOrCreateUserState(userKey); - - var requestStateObjects = - new StateObjectCollection(ExecutionScope.Request); - - var stateObjects = new[] - { - _globalStateObjects, - userStateObjects, - requestStateObjects - }; - - return new DataLoaderState( - services, _dataLoaderDescriptors, stateObjects); - } - - private StateObjectCollection GetOrCreateUserState( - string userKey) - { - return _cache.GetOrCreate(userKey, - () => new StateObjectCollection(ExecutionScope.User)); - } - } -} diff --git a/src/Runtime/ExecutionScope.cs b/src/Runtime/ExecutionScope.cs index aff7f94ab02..e3d390a1ccb 100644 --- a/src/Runtime/ExecutionScope.cs +++ b/src/Runtime/ExecutionScope.cs @@ -2,8 +2,7 @@ namespace HotChocolate.Runtime { public enum ExecutionScope { - Request = 0, - User = 1, - Global = 2 + Request, + Global } } diff --git a/src/Runtime/ICustomContextProvider.cs b/src/Runtime/ICustomContextProvider.cs new file mode 100644 index 00000000000..88e56547460 --- /dev/null +++ b/src/Runtime/ICustomContextProvider.cs @@ -0,0 +1,10 @@ +using System; + +namespace HotChocolate.Runtime +{ + public interface ICustomContextProvider + : IDisposable + { + T GetCustomContext(); + } +} diff --git a/src/Runtime/IDataLoaderState.cs b/src/Runtime/IDataLoaderProvider.cs similarity index 91% rename from src/Runtime/IDataLoaderState.cs rename to src/Runtime/IDataLoaderProvider.cs index 22cf6994837..f95d8c42a70 100644 --- a/src/Runtime/IDataLoaderState.cs +++ b/src/Runtime/IDataLoaderProvider.cs @@ -1,8 +1,10 @@ +using System; using System.Collections.Generic; namespace HotChocolate.Runtime { - public interface IDataLoaderState + public interface IDataLoaderProvider + : IDisposable { /// /// Gets the data loaders that have been requested since the last reset. diff --git a/src/Runtime/ISession.cs b/src/Runtime/ISession.cs new file mode 100644 index 00000000000..731a685fb02 --- /dev/null +++ b/src/Runtime/ISession.cs @@ -0,0 +1,11 @@ +using System; + +namespace HotChocolate.Runtime +{ + public interface ISession + : IDisposable + { + IDataLoaderProvider DataLoaders { get; } + ICustomContextProvider CustomContexts { get; } + } +} diff --git a/src/Runtime/ISessionManager.cs b/src/Runtime/ISessionManager.cs new file mode 100644 index 00000000000..8937c42e3f6 --- /dev/null +++ b/src/Runtime/ISessionManager.cs @@ -0,0 +1,10 @@ +using System; + +namespace HotChocolate.Runtime +{ + public interface ISessionManager + : IDisposable + { + ISession CreateSession(IServiceProvider requestServices); + } +} diff --git a/src/Runtime/ServiceFactory.cs b/src/Runtime/ServiceFactory.cs index a79a9a54153..9aa55750f55 100644 --- a/src/Runtime/ServiceFactory.cs +++ b/src/Runtime/ServiceFactory.cs @@ -45,7 +45,7 @@ private FactoryInfo CreateFactoryInfo( { throw new InvalidOperationException( $"The instance type `{type.FullName}` " + - "must have at least on constructor."); + "must have at least on public constructor."); } if (services == null) diff --git a/src/Runtime/Session.cs b/src/Runtime/Session.cs new file mode 100644 index 00000000000..dca99692e63 --- /dev/null +++ b/src/Runtime/Session.cs @@ -0,0 +1,82 @@ +using System; + +namespace HotChocolate.Runtime +{ + public sealed class Session + : ISession + { + private readonly object _sync = new object(); + private readonly Func _dataLoadersFactory; + private readonly Func _customContextsFactory; + + private IDataLoaderProvider _dataLoaders; + private ICustomContextProvider _customContexts; + private bool _disposed; + + public Session( + Func dataLoadersFactory, + Func customContextsFactory) + { + _dataLoadersFactory = dataLoadersFactory + ?? throw new ArgumentNullException(nameof(dataLoadersFactory)); + _customContextsFactory = customContextsFactory + ?? throw new ArgumentNullException(nameof(customContextsFactory)); + } + + public IDataLoaderProvider DataLoaders + { + get + { + if (_disposed) + { + throw new ObjectDisposedException("Session"); + } + + if (_dataLoaders == null) + { + lock (_sync) + { + if (_dataLoaders == null) + { + _dataLoaders = _dataLoadersFactory(); + } + } + } + return _dataLoaders; + } + } + + public ICustomContextProvider CustomContexts + { + get + { + if (_disposed) + { + throw new ObjectDisposedException("Session"); + } + + if (_customContexts == null) + { + lock (_sync) + { + if (_customContexts == null) + { + _customContexts = _customContextsFactory(); + } + } + } + return _customContexts; + } + } + + public void Dispose() + { + if (!_disposed) + { + _dataLoaders?.Dispose(); + _customContexts?.Dispose(); + _disposed = true; + } + } + } +} diff --git a/src/Runtime/SessionManager.cs b/src/Runtime/SessionManager.cs new file mode 100644 index 00000000000..6d609391a1a --- /dev/null +++ b/src/Runtime/SessionManager.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; + +namespace HotChocolate.Runtime +{ + public sealed class SessionManager + : ISessionManager + { + private readonly IServiceProvider _globalServices; + private readonly StateObjectDescriptorCollection _dataLoaderDescriptors; + private readonly StateObjectCollection _globalDataLoaders; + private readonly StateObjectDescriptorCollection _customContextDescriptors; + private readonly StateObjectCollection _globalCustomContexts; + private bool _disposed; + + public SessionManager( + IServiceProvider globalServices, + IEnumerable dataLoaderDescriptors, + IEnumerable customContextDescriptors) + { + if (dataLoaderDescriptors == null) + { + throw new ArgumentNullException(nameof(dataLoaderDescriptors)); + } + + if (customContextDescriptors == null) + { + throw new ArgumentNullException(nameof(customContextDescriptors)); + } + + _globalServices = globalServices + ?? throw new ArgumentNullException(nameof(globalServices)); + _dataLoaderDescriptors = + new StateObjectDescriptorCollection( + dataLoaderDescriptors); + _customContextDescriptors = + new StateObjectDescriptorCollection( + customContextDescriptors); + _globalDataLoaders = new StateObjectCollection( + ExecutionScope.Global); + _globalCustomContexts = new StateObjectCollection( + ExecutionScope.Global); + } + + public ISession CreateSession(IServiceProvider requestServices) + { + return new Session( + () => CreateDataLoaderProvider(requestServices), + () => CreateCustomContextProvider(requestServices)); + } + + private IDataLoaderProvider CreateDataLoaderProvider( + IServiceProvider requestServices) + { + return new DataLoaderProvider( + _globalServices, requestServices, + _dataLoaderDescriptors, _globalDataLoaders); + } + + private ICustomContextProvider CreateCustomContextProvider( + IServiceProvider requestServices) + { + return new CustomContextProvider( + _globalServices, requestServices, + _customContextDescriptors, _globalCustomContexts); + } + + public void Dispose() + { + if (!_disposed) + { + _globalDataLoaders.Dispose(); + _globalCustomContexts.Dispose(); + _disposed = true; + } + } + } +} diff --git a/src/Runtime/StateObjectCollection.cs b/src/Runtime/StateObjectCollection.cs index 240cf25cae7..e75e1f6db3c 100644 --- a/src/Runtime/StateObjectCollection.cs +++ b/src/Runtime/StateObjectCollection.cs @@ -72,6 +72,7 @@ protected virtual void Dispose(bool disposing) { disposable.Dispose(); } + _disposed = true; } } } diff --git a/src/Runtime/StateObjectContainer.cs b/src/Runtime/StateObjectContainer.cs index 044a3ee7082..34125bda0c9 100644 --- a/src/Runtime/StateObjectContainer.cs +++ b/src/Runtime/StateObjectContainer.cs @@ -5,76 +5,53 @@ namespace HotChocolate.Runtime { public class StateObjectContainer + : IDisposable { - private readonly IServiceProvider _root; + private readonly IServiceProvider _globalServices; + private readonly IServiceProvider _requestServices; private readonly StateObjectDescriptorCollection _descriptors; - private readonly Dictionary> _scopes; + private readonly StateObjectCollection _globalStates; + private readonly StateObjectCollection _requestStates; + private bool _disposed; protected StateObjectContainer( - IServiceProvider root, + IServiceProvider globalServices, + IServiceProvider requestServices, StateObjectDescriptorCollection descriptors, - ISet scopes, - IEnumerable> objectCollections) + StateObjectCollection globalStates) { - if (scopes == null) - { - throw new ArgumentNullException(nameof(scopes)); - } - - if (objectCollections == null) - { - throw new ArgumentNullException(nameof(objectCollections)); - } - - _root = root - ?? throw new ArgumentNullException(nameof(root)); + _globalServices = globalServices + ?? throw new ArgumentNullException(nameof(globalServices)); _descriptors = descriptors ?? throw new ArgumentNullException(nameof(descriptors)); - - _scopes = - new Dictionary>(); - - foreach (StateObjectCollection collection in - objectCollections) - { - _scopes[collection.Scope] = collection; - } - - foreach (ExecutionScope scope in scopes) - { - if (!_scopes.ContainsKey(scope)) - { - _scopes[scope] = new StateObjectCollection(scope); - } - } - } - - protected StateObjectContainer( - IServiceProvider root, - StateObjectDescriptorCollection descriptors, - ISet scopes) - : this(root, descriptors, scopes, - Enumerable.Empty>()) - { + _globalStates = globalStates + ?? throw new ArgumentNullException(nameof(globalStates)); + _requestServices = requestServices ?? globalServices; + _requestStates = new StateObjectCollection( + ExecutionScope.Request); } - public IEnumerable> Scopes => - _scopes.Values; - protected object GetStateObject(TKey key) { if (_descriptors.TryGetDescriptor(key, out IScopedStateDescriptor descriptor)) { - StateObjectCollection objects = _scopes[descriptor.Scope]; + IServiceProvider services = _globalServices; + StateObjectCollection stateObjects = _globalStates; + + if (descriptor.Scope == ExecutionScope.Request) + { + services = _requestServices; + stateObjects = _requestStates; + } - if (objects.TryGetObject(key, out object instance)) + if (stateObjects.TryGetObject(key, out object instance)) { return instance; } - return objects.CreateObject( - descriptor, _descriptors.CreateFactory(_root, descriptor)); + return stateObjects.CreateObject(descriptor, + _descriptors.CreateFactory(services, descriptor)); } return null; @@ -94,5 +71,20 @@ protected bool TryGetStateObjectDescriptor(TKey key, out T descriptor) descriptor = default(T); return false; } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (!_disposed && disposing) + { + _requestStates.Dispose(); + _disposed = true; + } + } } } diff --git a/src/Runtime/StateObjectDescriptorCollection.cs b/src/Runtime/StateObjectDescriptorCollection.cs index cf1a3736f2f..c18a14fe39e 100644 --- a/src/Runtime/StateObjectDescriptorCollection.cs +++ b/src/Runtime/StateObjectDescriptorCollection.cs @@ -31,7 +31,7 @@ public Func CreateFactory( IServiceProvider services, IScopedStateDescriptor descriptor) { - if (descriptor.Factory == null) + if (descriptor.Factory != null) { return () => descriptor.Factory(services); } diff --git a/src/Runtime/TestSettings.cs b/src/Runtime/TestSettings.cs new file mode 100644 index 00000000000..f2121efd1da --- /dev/null +++ b/src/Runtime/TestSettings.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("HotChocolate.Runtime.Tests")] diff --git a/src/Settings.props b/src/Settings.props index 114fea1cada..bf6ed5b1849 100644 --- a/src/Settings.props +++ b/src/Settings.props @@ -1,5 +1,7 @@ + - 7.3 + 7.2 + diff --git a/src/Types/Configuration/DirectiveRegistry.cs b/src/Types/Configuration/DirectiveRegistry.cs index 4abc2c8d7e8..b7b3100f1a6 100644 --- a/src/Types/Configuration/DirectiveRegistry.cs +++ b/src/Types/Configuration/DirectiveRegistry.cs @@ -25,7 +25,7 @@ public void RegisterDirective(T directive) where T : Directive _directives.Add(directive); } - public IEnumerable GetDirectives() + public IReadOnlyCollection GetDirectives() { return _directives; } diff --git a/src/Types/Configuration/ICustomContextConfiguration.cs b/src/Types/Configuration/ICustomContextConfiguration.cs new file mode 100644 index 00000000000..f786a7596d9 --- /dev/null +++ b/src/Types/Configuration/ICustomContextConfiguration.cs @@ -0,0 +1,13 @@ +using System; +using HotChocolate.Runtime; + +namespace HotChocolate +{ + public interface ICustomContextConfiguration + : IFluent + { + void RegisterCustomContext( + ExecutionScope scope, + Func contextFactory = null); + } +} diff --git a/src/Types/Configuration/IDataLoaderConfiguration.cs b/src/Types/Configuration/IDataLoaderConfiguration.cs index 28cf8627782..4a6c8ecefaa 100644 --- a/src/Types/Configuration/IDataLoaderConfiguration.cs +++ b/src/Types/Configuration/IDataLoaderConfiguration.cs @@ -6,8 +6,9 @@ namespace HotChocolate.Configuration { public interface IDataLoaderConfiguration + : IFluent { - void RegisterLoader( + void RegisterDataLoader( string key, ExecutionScope scope, Func loaderFactory = null, diff --git a/src/Types/Configuration/IDirectiveRegistry.cs b/src/Types/Configuration/IDirectiveRegistry.cs index d622dd68e97..2e8eb30dc01 100644 --- a/src/Types/Configuration/IDirectiveRegistry.cs +++ b/src/Types/Configuration/IDirectiveRegistry.cs @@ -9,6 +9,6 @@ internal interface IDirectiveRegistry void RegisterDirective(T directive) where T : Directive; - IEnumerable GetDirectives(); + IReadOnlyCollection GetDirectives(); } } diff --git a/src/Types/Configuration/ISchemaContext.cs b/src/Types/Configuration/ISchemaContext.cs index 2b04b95034b..51d6ca0ec5b 100644 --- a/src/Types/Configuration/ISchemaContext.cs +++ b/src/Types/Configuration/ISchemaContext.cs @@ -11,5 +11,6 @@ internal interface ISchemaContext IDirectiveRegistry Directives { get; } IResolverRegistry Resolvers { get; } ICollection DataLoaders { get; } + ICollection CustomContexts { get; } } } diff --git a/src/Types/Configuration/SchemaConfiguration.CustomContext.cs b/src/Types/Configuration/SchemaConfiguration.CustomContext.cs new file mode 100644 index 00000000000..51239d7f428 --- /dev/null +++ b/src/Types/Configuration/SchemaConfiguration.CustomContext.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using HotChocolate.Resolvers; +using HotChocolate.Runtime; + +namespace HotChocolate.Configuration +{ + internal partial class SchemaConfiguration + { + private Dictionary _customContexts = + new Dictionary(); + + internal IReadOnlyCollection + CustomContextDescriptors => _customContexts.Values; + + public void RegisterCustomContext( + ExecutionScope scope, + Func contextFactory = null) + { + Func factory = null; + if (contextFactory != null) + { + factory = new Func( + sp => contextFactory(sp)); + } + + var descriptor = new CustomContextDescriptor( + typeof(T), factory, scope); + _customContexts[typeof(T)] = descriptor; + } + } +} diff --git a/src/Types/Configuration/SchemaConfiguration.DataLoader.cs b/src/Types/Configuration/SchemaConfiguration.DataLoader.cs index 9857bf5f488..4c73add3423 100644 --- a/src/Types/Configuration/SchemaConfiguration.DataLoader.cs +++ b/src/Types/Configuration/SchemaConfiguration.DataLoader.cs @@ -8,7 +8,6 @@ namespace HotChocolate.Configuration { internal partial class SchemaConfiguration - : ISchemaConfiguration { private Dictionary _dataLoaders = new Dictionary(); @@ -16,7 +15,7 @@ internal partial class SchemaConfiguration internal IReadOnlyCollection DataLoaderDescriptors => _dataLoaders.Values; - public void RegisterLoader( + public void RegisterDataLoader( string key, ExecutionScope scope, Func loaderFactory = null, @@ -27,10 +26,24 @@ public void RegisterLoader( throw new ArgumentNullException(nameof(key)); } + Func factory = null; + if (loaderFactory != null) + { + factory = new Func( + sp => loaderFactory(sp)); + } + + TriggerDataLoaderAsync trigger = null; + if (triggerLoaderAsync != null) + { + trigger = new TriggerDataLoaderAsync( + (o, c) => triggerLoaderAsync((T)o, c)); + } + var descriptor = new DataLoaderDescriptor( key, typeof(T), scope, - sp => loaderFactory(sp), - (o, c) => triggerLoaderAsync((T)o, c)); + factory, + trigger); _dataLoaders[key] = descriptor; } } diff --git a/src/Types/Configuration/SchemaContext.cs b/src/Types/Configuration/SchemaContext.cs index 36c813f2e0f..c545f6d6936 100644 --- a/src/Types/Configuration/SchemaContext.cs +++ b/src/Types/Configuration/SchemaContext.cs @@ -22,6 +22,7 @@ public SchemaContext() _resolverRegistry = new ResolverRegistry(); _directiveRegistry = new DirectiveRegistry(); DataLoaders = new List(); + CustomContexts = new List(); } public ITypeRegistry Types => _typeRegistry; @@ -34,6 +35,8 @@ public SchemaContext() public ICollection DataLoaders { get; } + public ICollection CustomContexts { get; } + public IEnumerable CompleteTypes() { // compile resolvers diff --git a/src/Types/Configuration/TypeInitializers/TypeFinalizer.cs b/src/Types/Configuration/TypeInitializers/TypeFinalizer.cs index d260eedd02a..0f6e16451e8 100644 --- a/src/Types/Configuration/TypeInitializers/TypeFinalizer.cs +++ b/src/Types/Configuration/TypeInitializers/TypeFinalizer.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using HotChocolate.Runtime; using HotChocolate.Types; namespace HotChocolate.Configuration @@ -29,6 +30,9 @@ public void FinalizeTypes(SchemaContext context, string queryTypeName) // finalize and register field resolver bindings RegisterFieldResolvers(context); + // register state object descriptors + RegisterStateObjects(context); + // compile resolvers and finalize types _errors.AddRange(context.CompleteTypes()); _errors.AddRange(context.CompleteDirectives()); @@ -56,5 +60,20 @@ private void RegisterFieldResolvers(ISchemaContext context) _schemaConfiguration.ResolverBindings); resolverRegistrar.RegisterResolvers(context); } + + private void RegisterStateObjects(ISchemaContext context) + { + foreach (DataLoaderDescriptor descriptor in + _schemaConfiguration.DataLoaderDescriptors) + { + context.DataLoaders.Add(descriptor); + } + + foreach (CustomContextDescriptor descriptor in + _schemaConfiguration.CustomContextDescriptors) + { + context.CustomContexts.Add(descriptor); + } + } } } diff --git a/src/Types/DataLoaderConfigurationExtensions.cs b/src/Types/DataLoaderConfigurationExtensions.cs deleted file mode 100644 index 7d34ff4a262..00000000000 --- a/src/Types/DataLoaderConfigurationExtensions.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using HotChocolate.Configuration; -using HotChocolate.Runtime; - -namespace HotChocolate -{ - public static class DataLoaderConfigurationExtensions - { - public static void RegisterLoader( - this IDataLoaderConfiguration configuration, - Func loaderFactory = null, - Func triggerLoaderAsync = null) - { - RegisterLoader(configuration, ExecutionScope.Request); - } - - public static void RegisterLoader( - this IDataLoaderConfiguration configuration, - ExecutionScope scope, - Func loaderFactory = null, - Func triggerLoaderAsync = null) - { - configuration.RegisterLoader( - typeof(T).FullName, scope, loaderFactory, triggerLoaderAsync); - } - - public static void RegisterLoader( - this IDataLoaderConfiguration configuration, - string key, - Func loaderFactory = null, - Func triggerLoaderAsync = null) - { - configuration.RegisterLoader(key, ExecutionScope.Request, - loaderFactory, triggerLoaderAsync); - } - } -} diff --git a/src/Types/ISchema.cs b/src/Types/ISchema.cs index b8304198ded..b60f28783b1 100644 --- a/src/Types/ISchema.cs +++ b/src/Types/ISchema.cs @@ -12,6 +12,7 @@ namespace HotChocolate /// the entry points for query, mutation, and subscription operations. /// public interface ISchema + : IDisposable { /// /// Gets the schema options. @@ -51,14 +52,10 @@ public interface ISchema IReadOnlyCollection Directives { get; } /// - /// Gets the data loader descriptors. + /// Gets the session manager which can be used to create + /// new query execution sessions. /// - IReadOnlyCollection DataLoaders { get; } - - /// - /// Gets the state object descriptors. - /// - IReadOnlyCollection StateObjects { get; } + ISessionManager Sessions { get; } /// /// Gets a type by its name and kind. diff --git a/src/Types/ISchemaConfiguration.cs b/src/Types/ISchemaConfiguration.cs index d4f016ad29f..ee5e0b68512 100644 --- a/src/Types/ISchemaConfiguration.cs +++ b/src/Types/ISchemaConfiguration.cs @@ -7,15 +7,10 @@ public interface ISchemaConfiguration : ISchemaFirstConfiguration , ICodeFirstConfiguration , IDataLoaderConfiguration - , IUserStateConfiguration + , ICustomContextConfiguration { ISchemaOptions Options { get; } void RegisterServiceProvider(IServiceProvider serviceProvider); } - - public interface IUserStateConfiguration - { - - } } diff --git a/src/Types/Internal/DotNetTypeInfoFactory.cs b/src/Types/Internal/DotNetTypeInfoFactory.cs index 75c8c78f12c..58fb56e4499 100644 --- a/src/Types/Internal/DotNetTypeInfoFactory.cs +++ b/src/Types/Internal/DotNetTypeInfoFactory.cs @@ -135,6 +135,11 @@ private static Type RemoveNonEssentialParts(Type type) current = GetInnerType(current); } + if (IsResoverResultType(current)) + { + current = GetInnerType(current); + } + return current; } @@ -164,7 +169,8 @@ private static Type GetInnerType(Type type) if (IsTaskType(type) || IsNullableType(type) - || IsWrapperType(type)) + || IsWrapperType(type) + || IsResoverResultType(type)) { return type.GetGenericArguments().First(); } @@ -221,6 +227,13 @@ private static bool IsTaskType(Type type) && typeof(Task<>) == type.GetGenericTypeDefinition(); } + private static bool IsResoverResultType(Type type) + { + return type.IsGenericType + && (typeof(IResolverResult<>) == type.GetGenericTypeDefinition() + || typeof(ResolverResult<>) == type.GetGenericTypeDefinition()); + } + public static bool IsNullableType(Type type) { return type.IsGenericType diff --git a/src/Types/Resolvers/CodeGeneration/SourceCodeGenerator.cs b/src/Types/Resolvers/CodeGeneration/SourceCodeGenerator.cs index 1f907fe0fea..6aecac3b6e8 100644 --- a/src/Types/Resolvers/CodeGeneration/SourceCodeGenerator.cs +++ b/src/Types/Resolvers/CodeGeneration/SourceCodeGenerator.cs @@ -74,6 +74,12 @@ private void GenerateArgumentInvocation( case FieldResolverArgumentKind.CancellationToken: source.Append($"ct"); break; + case FieldResolverArgumentKind.DataLoader: + source.Append($"ctx.{nameof(IResolverContext.DataLoader)}<{GetTypeName(argumentDescriptor.Type)}>()"); + break; + case FieldResolverArgumentKind.CustomContext: + source.Append($"ctx.{nameof(IResolverContext.CustomContext)}<{GetTypeName(argumentDescriptor.Type)}>()"); + break; default: throw new NotSupportedException(); } diff --git a/src/Types/Resolvers/FieldResolverArgumentDescriptor.cs b/src/Types/Resolvers/FieldResolverArgumentDescriptor.cs index 2d09b27b366..e468d1e2558 100644 --- a/src/Types/Resolvers/FieldResolverArgumentDescriptor.cs +++ b/src/Types/Resolvers/FieldResolverArgumentDescriptor.cs @@ -9,7 +9,8 @@ public class FieldResolverArgumentDescriptor { internal FieldResolverArgumentDescriptor( string name, string variableName, - FieldResolverArgumentKind kind, Type type) + FieldResolverArgumentKind kind, + Type type) { Name = name; VariableName = variableName; @@ -37,58 +38,5 @@ internal FieldResolverArgumentDescriptor( /// Gets the argument type. /// public Type Type { get; } - - internal static FieldResolverArgumentKind LookupKind( - Type argumentType, Type sourceType) - { - if (argumentType == sourceType) - { - return FieldResolverArgumentKind.Source; - } - - if (argumentType == typeof(IResolverContext)) - { - return FieldResolverArgumentKind.Context; - } - - if (argumentType == typeof(Schema)) - { - return FieldResolverArgumentKind.Schema; - } - - if (argumentType == typeof(ObjectType)) - { - return FieldResolverArgumentKind.ObjectType; - } - - if (argumentType == typeof(ObjectField)) - { - return FieldResolverArgumentKind.Field; - } - - if (argumentType == typeof(CancellationToken)) - { - return FieldResolverArgumentKind.CancellationToken; - } - - if (argumentType == typeof(DocumentNode)) - { - return FieldResolverArgumentKind.QueryDocument; - } - - if (argumentType == typeof(OperationDefinitionNode)) - { - return FieldResolverArgumentKind.OperationDefinition; - } - - if (argumentType == typeof(FieldNode)) - { - return FieldResolverArgumentKind.Field; - } - - // Service -> Attribute - - return FieldResolverArgumentKind.Argument; - } } } diff --git a/src/Types/Resolvers/FieldResolverArgumentHelper.cs b/src/Types/Resolvers/FieldResolverArgumentHelper.cs new file mode 100644 index 00000000000..5b588ddaedf --- /dev/null +++ b/src/Types/Resolvers/FieldResolverArgumentHelper.cs @@ -0,0 +1,159 @@ +using System; +using System.Reflection; +using System.Threading; +using HotChocolate.Language; +using HotChocolate.Types; + +namespace HotChocolate.Resolvers +{ + internal static class FieldResolverArgumentHelper + { + internal static FieldResolverArgumentKind LookupKind( + ParameterInfo parameter, Type sourceType) + { + FieldResolverArgumentKind argumentKind; + if (TryCheckForResolverArguments( + parameter, sourceType, out argumentKind) + || TryCheckForSchemaTypes(parameter, out argumentKind) + || TryCheckForQueryTypes(parameter, out argumentKind) + || TryCheckForExtensions(parameter, out argumentKind)) + { + return argumentKind; + } + + return FieldResolverArgumentKind.Argument; + } + + private static bool TryCheckForResolverArguments( + this ParameterInfo parameter, + Type sourceType, + out FieldResolverArgumentKind argumentKind) + { + if (parameter.ParameterType == sourceType) + { + argumentKind = FieldResolverArgumentKind.Source; + return true; + } + + if (parameter.ParameterType == typeof(IResolverContext)) + { + argumentKind = FieldResolverArgumentKind.Context; + return true; + } + + if (parameter.ParameterType == typeof(CancellationToken)) + { + argumentKind = FieldResolverArgumentKind.CancellationToken; + return true; + } + + argumentKind = default(FieldResolverArgumentKind); + return false; + } + + private static bool TryCheckForSchemaTypes( + this ParameterInfo parameter, + out FieldResolverArgumentKind argumentKind) + { + if (parameter.ParameterType == typeof(ISchema)) + { + argumentKind = FieldResolverArgumentKind.Schema; + return true; + } + + if (parameter.ParameterType == typeof(Schema)) + { + argumentKind = FieldResolverArgumentKind.Schema; + return true; + } + + if (parameter.ParameterType == typeof(ObjectType)) + { + argumentKind = FieldResolverArgumentKind.ObjectType; + return true; + } + + if (parameter.ParameterType == typeof(IOutputField)) + { + argumentKind = FieldResolverArgumentKind.Field; + return true; + } + + if (parameter.ParameterType == typeof(ObjectField)) + { + argumentKind = FieldResolverArgumentKind.Field; + return true; + } + + argumentKind = default(FieldResolverArgumentKind); + return false; + } + + private static bool TryCheckForQueryTypes( + this ParameterInfo parameter, + out FieldResolverArgumentKind argumentKind) + { + if (parameter.ParameterType == typeof(DocumentNode)) + { + argumentKind = FieldResolverArgumentKind.QueryDocument; + return true; + } + + if (parameter.ParameterType == typeof(OperationDefinitionNode)) + { + argumentKind = FieldResolverArgumentKind.OperationDefinition; + return true; + } + + if (parameter.ParameterType == typeof(FieldNode)) + { + argumentKind = FieldResolverArgumentKind.FieldSelection; + return true; + } + + argumentKind = default(FieldResolverArgumentKind); + return false; + } + + private static bool TryCheckForExtensions( + this ParameterInfo parameter, + out FieldResolverArgumentKind argumentKind) + { + if (parameter.IsDataLoader()) + { + argumentKind = FieldResolverArgumentKind.DataLoader; + return true; + } + + if (parameter.IsState()) + { + argumentKind = FieldResolverArgumentKind.CustomContext; + return true; + } + + if (parameter.IsService()) + { + argumentKind = FieldResolverArgumentKind.Service; + return true; + } + + argumentKind = default(FieldResolverArgumentKind); + return false; + } + + private static bool IsDataLoader(this ParameterInfo parameter) + { + return parameter.IsDefined(typeof(DataLoaderAttribute)); + } + + private static bool IsState(this ParameterInfo parameter) + { + return parameter.IsDefined(typeof(StateAttribute)); + } + + private static bool IsService(this ParameterInfo parameter) + { + return parameter.IsDefined(typeof(ServiceAttribute)); + } + } +} diff --git a/src/Types/Resolvers/FieldResolverArgumentKind.cs b/src/Types/Resolvers/FieldResolverArgumentKind.cs index 9d1d4a44a8c..7a953e2abe3 100644 --- a/src/Types/Resolvers/FieldResolverArgumentKind.cs +++ b/src/Types/Resolvers/FieldResolverArgumentKind.cs @@ -12,6 +12,8 @@ public enum FieldResolverArgumentKind OperationDefinition, FieldSelection, Context, - CancellationToken + CancellationToken, + CustomContext, + DataLoader } } diff --git a/src/Types/Resolvers/FieldResolverDiscoverer.cs b/src/Types/Resolvers/FieldResolverDiscoverer.cs index d60eb31e262..874475502d8 100644 --- a/src/Types/Resolvers/FieldResolverDiscoverer.cs +++ b/src/Types/Resolvers/FieldResolverDiscoverer.cs @@ -102,8 +102,8 @@ internal static IReadOnlyCollection CreateResol foreach (ParameterInfo parameter in method.GetParameters()) { - FieldResolverArgumentKind kind = FieldResolverArgumentDescriptor - .LookupKind(parameter.ParameterType, sourceType); + FieldResolverArgumentKind kind = FieldResolverArgumentHelper + .LookupKind(parameter, sourceType); string variableName = $"v{i++}_{parameter.Name}"; arguments.Add(new FieldResolverArgumentDescriptor( parameter.Name, variableName, kind, parameter.ParameterType)); diff --git a/src/Types/Resolvers/IResolverContext.cs b/src/Types/Resolvers/IResolverContext.cs index 729f90ea9dc..1fc8540162c 100644 --- a/src/Types/Resolvers/IResolverContext.cs +++ b/src/Types/Resolvers/IResolverContext.cs @@ -32,8 +32,8 @@ public interface IResolverContext T Service(); - T State(); + T CustomContext(); - T Loader(string key); + T DataLoader(string key); } } diff --git a/src/Types/Resolvers/ResolverContextExtensions.cs b/src/Types/Resolvers/ResolverContextExtensions.cs index 003b7144cfd..82f8d7174df 100644 --- a/src/Types/Resolvers/ResolverContextExtensions.cs +++ b/src/Types/Resolvers/ResolverContextExtensions.cs @@ -2,9 +2,9 @@ namespace HotChocolate.Resolvers { public static class ResolverContextExtensions { - public static T Loader(this IResolverContext resolverContext) + public static T DataLoader(this IResolverContext resolverContext) { - return resolverContext.Loader(typeof(T).FullName); + return resolverContext.DataLoader(typeof(T).FullName); } } } diff --git a/src/Types/Schema.Factory.cs b/src/Types/Schema.Factory.cs index e3072ce97e6..9c88710cae2 100644 --- a/src/Types/Schema.Factory.cs +++ b/src/Types/Schema.Factory.cs @@ -95,12 +95,7 @@ private static Schema CreateSchema( return new Schema( context.Services ?? new ServiceFactory(), - SchemaTypes.Create( - context.Types.GetTypes(), - context.Types.GetTypeBindings(), - options), - context.Directives.GetDirectives().ToArray(), - context.DataLoaders.ToArray(), + context, options); } diff --git a/src/Types/Schema.cs b/src/Types/Schema.cs index 5d4c5c9cc0b..881b8711ce1 100644 --- a/src/Types/Schema.cs +++ b/src/Types/Schema.cs @@ -18,19 +18,24 @@ public partial class Schema : ISchema { private readonly SchemaTypes _types; + private bool _disposed; private Schema( IServiceProvider services, - SchemaTypes types, - IReadOnlyCollection directives, - IReadOnlyCollection dataLoaders, + ISchemaContext context, IReadOnlySchemaOptions options) { - _types = types; Services = services; - Directives = directives; - DataLoaders = dataLoaders; + _types = SchemaTypes.Create( + context.Types.GetTypes(), + context.Types.GetTypeBindings(), + options); + Directives = context.Directives.GetDirectives(); Options = options; + Sessions = new SessionManager( + services, + context.DataLoaders, + context.CustomContexts); } /// @@ -71,14 +76,10 @@ private Schema( public IReadOnlyCollection Directives { get; } /// - /// Gets the data loader descriptors. + /// Gets the session manager which can be used to create + /// new query execution sessions. /// - public IReadOnlyCollection DataLoaders { get; } - - /// - /// Gets the state object descriptors. - /// - public IReadOnlyCollection StateObjects { get; } + public ISessionManager Sessions { get; } /// /// Gets a type by its name and kind. @@ -157,5 +158,20 @@ public IReadOnlyCollection GetPossibleTypes( return Array.Empty(); } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (!_disposed && disposing) + { + Sessions.Dispose(); + _disposed = true; + } + } } } diff --git a/src/Types/SchemaException.cs b/src/Types/SchemaException.cs index 301b16937cb..375b3d7a21f 100644 --- a/src/Types/SchemaException.cs +++ b/src/Types/SchemaException.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; +using System.Text; namespace HotChocolate { @@ -11,12 +13,14 @@ public SchemaException(params SchemaError[] errors) : base(CreateErrorMessage(errors)) { Errors = errors; + Debug.WriteLine(Message); } public SchemaException(IEnumerable errors) : base(CreateErrorMessage(errors.ToArray())) { Errors = errors.ToArray(); + Debug.WriteLine(Message); } public IReadOnlyCollection Errors { get; } @@ -28,14 +32,32 @@ private static string CreateErrorMessage( { return "Unexpected schema exception occured."; } - else if (errors.Count == 1) + + if (errors.Count == 1) + { + return CreateErrorMessage(errors.First()); + } + + var message = new StringBuilder(); + + message.AppendLine("Multiple schema errors occured:"); + foreach (SchemaError error in errors) + { + message.AppendLine(CreateErrorMessage(error)); + } + + return message.ToString(); + } + + private static string CreateErrorMessage(SchemaError error) + { + if (error.Type == null) { - return errors.First().Message; + return error.Message; } else { - return "Multiple schema errors occured. " + - "For more details check `Errors` property."; + return $"{error.Message} - Type: {error.Type.Name}"; } } } diff --git a/src/Types/Types/Descriptors/ObjectFieldDescriptor.cs b/src/Types/Types/Descriptors/ObjectFieldDescriptor.cs index 45ad8da8779..94dec51d4cc 100644 --- a/src/Types/Types/Descriptors/ObjectFieldDescriptor.cs +++ b/src/Types/Types/Descriptors/ObjectFieldDescriptor.cs @@ -98,7 +98,7 @@ private IEnumerable CreateArguments() { string argumentName = parameter.GetGraphQLName(); if (!descriptions.ContainsKey(argumentName) - && IsArgumentType(parameter.ParameterType)) + && IsArgumentType(parameter)) { var argumentDescriptor = new ArgumentDescriptor(argumentName, @@ -112,10 +112,10 @@ private IEnumerable CreateArguments() return descriptions.Values; } - private bool IsArgumentType(Type argumentType) + private bool IsArgumentType(ParameterInfo parameter) { - return (FieldResolverArgumentDescriptor - .LookupKind(argumentType, FieldDescription.Member.ReflectedType) == + return (FieldResolverArgumentHelper + .LookupKind(parameter, FieldDescription.Member.ReflectedType) == FieldResolverArgumentKind.Argument); }