diff --git a/src/GraphQL.Extensions.Experimental/GraphQL.Extensions.Experimental.csproj b/src/GraphQL.Extensions.Experimental/GraphQL.Extensions.Experimental.csproj
new file mode 100644
index 000000000..6cf67e9e3
--- /dev/null
+++ b/src/GraphQL.Extensions.Experimental/GraphQL.Extensions.Experimental.csproj
@@ -0,0 +1,15 @@
+
+
+
+ net8.0
+ enable
+ enable
+ true
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/GraphQL.Extensions.Experimental/OneOf/OneOfDirective.cs b/src/GraphQL.Extensions.Experimental/OneOf/OneOfDirective.cs
new file mode 100644
index 000000000..c2ad41e0c
--- /dev/null
+++ b/src/GraphQL.Extensions.Experimental/OneOf/OneOfDirective.cs
@@ -0,0 +1,66 @@
+using Tanka.GraphQL.Language;
+using Tanka.GraphQL.Language.Nodes;
+using Tanka.GraphQL.Language.Nodes.TypeSystem;
+using Tanka.GraphQL.TypeSystem;
+using Tanka.GraphQL.Validation;
+
+namespace Tanka.GraphQL.Extensions.Experimental.OneOf;
+
+public class OneOfDirective
+{
+ private static readonly List AllowedKinds = [NodeKind.ObjectValue, NodeKind.Variable];
+
+ public static DirectiveDefinition Directive =>
+ $"directive @oneOf on {TypeSystemDirectiveLocations.INPUT_OBJECT}";
+
+ public static CombineRule OneOfValidationRule()
+ {
+ return (context, rule) =>
+ {
+ rule.EnterArgument += argument =>
+ {
+ InputValueDefinition? argumentDefinition = context.Tracker.ArgumentDefinition;
+
+ if (argumentDefinition is null)
+ return;
+
+ if (!AllowedKinds.Contains(argument.Value.Kind))
+ return;
+
+ if (context.Schema.GetNamedType(argumentDefinition.Type.Unwrap().Name) is not InputObjectDefinition
+ inputObject)
+ return;
+
+ if (!inputObject.HasDirective(Directive.Name))
+ return;
+
+ if (argument.Value.Kind == NodeKind.Variable)
+ {
+ var variable = (Variable)argument.Value;
+
+ if (context.VariableValues?.TryGetValue(variable.Name, out object? variableValue) != true) return;
+
+ if (variableValue is not null)
+ {
+ var coercedValue = Values.CoerceValue(
+ context.Schema,
+ variableValue,
+ argumentDefinition.Type) as IReadOnlyDictionary;
+
+ if (coercedValue?.Count(kv => kv.Value is not null) != 1)
+ context.Error("ONEOF001",
+ $"Invalid value for '@oneOf' input '{inputObject.Name}'. @oneOf input objects can only have one field value set.");
+ }
+ }
+ else
+ {
+ var objectValue = (ObjectValue)argument.Value;
+
+ if (objectValue.Count != 1)
+ context.Error("ONEOF001",
+ $"Invalid value for '@oneOf' input '{inputObject.Name}'. @oneOf input objects can only have one field value set.");
+ }
+ };
+ };
+ }
+}
\ No newline at end of file
diff --git a/src/GraphQL.Extensions.Experimental/OneOf/SchemaBuilderExtensions.cs b/src/GraphQL.Extensions.Experimental/OneOf/SchemaBuilderExtensions.cs
new file mode 100644
index 000000000..ba6a79a4f
--- /dev/null
+++ b/src/GraphQL.Extensions.Experimental/OneOf/SchemaBuilderExtensions.cs
@@ -0,0 +1,12 @@
+using Tanka.GraphQL.TypeSystem;
+
+namespace Tanka.GraphQL.Extensions.Experimental.OneOf;
+
+public static class SchemaBuilderExtensions
+{
+ public static SchemaBuilder AddOneOf(this SchemaBuilder builder)
+ {
+ builder.Add(OneOfDirective.Directive);
+ return builder;
+ }
+}
\ No newline at end of file
diff --git a/src/GraphQL.Extensions.Experimental/OneOf/ServiceCollectionExtensions.cs b/src/GraphQL.Extensions.Experimental/OneOf/ServiceCollectionExtensions.cs
new file mode 100644
index 000000000..98bc2868a
--- /dev/null
+++ b/src/GraphQL.Extensions.Experimental/OneOf/ServiceCollectionExtensions.cs
@@ -0,0 +1,17 @@
+using Microsoft.Extensions.DependencyInjection;
+
+using Tanka.GraphQL.Validation;
+
+namespace Tanka.GraphQL.Extensions.Experimental.OneOf;
+
+public static class ServiceCollectionExtensions
+{
+ public static IServiceCollection AddOneOf(this IServiceCollection services)
+ {
+ services.PostConfigure(
+ options => options.Rules.Add(OneOfDirective.OneOfValidationRule())
+ );
+
+ return services;
+ }
+}
\ No newline at end of file
diff --git a/src/GraphQL.Language/Nodes/ObjectValue.cs b/src/GraphQL.Language/Nodes/ObjectValue.cs
index 77f5ac546..bf138af81 100644
--- a/src/GraphQL.Language/Nodes/ObjectValue.cs
+++ b/src/GraphQL.Language/Nodes/ObjectValue.cs
@@ -3,33 +3,26 @@
namespace Tanka.GraphQL.Language.Nodes;
-public sealed class ObjectValue : ValueBase, ICollectionNode
+public sealed class ObjectValue(
+ IReadOnlyList fields,
+ in Location? location = default)
+ : ValueBase, ICollectionNode
{
- //todo: remove?
- public readonly IReadOnlyList Fields;
-
- public ObjectValue(
- IReadOnlyList fields,
- in Location? location = default)
- {
- Fields = fields;
- Location = location;
- }
-
public override NodeKind Kind => NodeKind.ObjectValue;
- public override Location? Location { get; }
+ public override Location? Location { get; } = location;
public IEnumerator GetEnumerator()
{
- return Fields.GetEnumerator();
+ return fields.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
- return ((IEnumerable)Fields).GetEnumerator();
+ return ((IEnumerable)fields).GetEnumerator();
}
- public int Count => Fields.Count;
- public ObjectField this[int index] => Fields[index];
+ public int Count => fields.Count;
+
+ public ObjectField this[int index] => fields[index];
}
\ No newline at end of file
diff --git a/src/GraphQL.Server/GraphQLApplicationBuilder.cs b/src/GraphQL.Server/GraphQLApplicationBuilder.cs
index 6c315c9e3..e794249f0 100644
--- a/src/GraphQL.Server/GraphQLApplicationBuilder.cs
+++ b/src/GraphQL.Server/GraphQLApplicationBuilder.cs
@@ -14,7 +14,7 @@ public GraphQLApplicationBuilder(IServiceCollection applicationServices)
ApplicationOptionsBuilder = ApplicationServices
.AddOptions();
- AddCore();
+ AddDefaultTankaGraphQLServerServices();
}
public IServiceCollection ApplicationServices { get; }
@@ -55,10 +55,11 @@ public GraphQLApplicationBuilder AddWebSockets()
return this;
}
- private void AddCore()
+ private void AddDefaultTankaGraphQLServerServices()
{
ApplicationServices.TryAddSingleton();
ApplicationServices.TryAddSingleton();
ApplicationServices.TryAddSingleton();
+ ApplicationServices.AddDefaultTankaGraphQLServices();
}
}
\ No newline at end of file
diff --git a/src/GraphQL.Server/WebSockets/ClientMethods.cs b/src/GraphQL.Server/WebSockets/ClientMethods.cs
index b8845861a..23b074936 100644
--- a/src/GraphQL.Server/WebSockets/ClientMethods.cs
+++ b/src/GraphQL.Server/WebSockets/ClientMethods.cs
@@ -1,17 +1,10 @@
-using System.Threading;
-using System.Threading.Channels;
-using System.Threading.Tasks;
+using System.Threading.Channels;
namespace Tanka.GraphQL.Server.WebSockets;
-public class ClientMethods
+public class ClientMethods(ChannelWriter writer)
{
- protected ChannelWriter Writer { get; }
-
- public ClientMethods(ChannelWriter writer)
- {
- Writer = writer;
- }
+ protected ChannelWriter Writer { get; } = writer;
public async Task ConnectionAck(ConnectionAck connectionAck, CancellationToken cancellationToken)
{
diff --git a/src/GraphQL.Server/WebSockets/ServerMethods.cs b/src/GraphQL.Server/WebSockets/ServerMethods.cs
index 51b6053b0..6776fd058 100644
--- a/src/GraphQL.Server/WebSockets/ServerMethods.cs
+++ b/src/GraphQL.Server/WebSockets/ServerMethods.cs
@@ -1,14 +1,17 @@
using System.Collections.Concurrent;
+using System.Diagnostics;
using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
-using Tanka.GraphQL.Fields;
+using Tanka.GraphQL.Request;
using Tanka.GraphQL.Server.WebSockets.WebSocketPipe;
using Tanka.GraphQL.Validation;
namespace Tanka.GraphQL.Server.WebSockets;
-public class ServerMethods
+public partial class ServerMethods
{
private readonly GraphQLRequestDelegate _requestDelegate;
private readonly HttpContext _httpContext;
@@ -20,11 +23,14 @@ public ServerMethods(WebSocketMessageChannel channel, GraphQLReques
_httpContext = httpContext;
Channel = channel;
Client = new ClientMethods(Channel.Writer);
+ _logger = httpContext.RequestServices.GetRequiredService>();
}
public ClientMethods Client { get; set; }
public ConcurrentDictionary Subscriptions = new();
+
+ private readonly ILogger _logger;
public async Task ConnectionInit(ConnectionInit connectionInit, CancellationToken cancellationToken)
{
@@ -57,6 +63,7 @@ await Channel.Complete(
private async Task Execute(Subscribe subscribe, CancellationTokenSource unsubscribeOrAborted)
{
+ _ = _logger.BeginScope(subscribe.Id);
var cancellationToken = unsubscribeOrAborted.Token;
var context = new GraphQLRequestContext
{
@@ -73,18 +80,27 @@ private async Task Execute(Subscribe subscribe, CancellationTokenSource unsubscr
try
{
+ ulong count = 0;
+ Log.Request(_logger, subscribe.Id, context.Request);
await _requestDelegate(context);
await using var enumerator = context.Response.GetAsyncEnumerator(cancellationToken);
+ long started = Stopwatch.GetTimestamp();
while (await enumerator.MoveNextAsync())
{
+ count++;
+ string elapsed = $"{Stopwatch.GetElapsedTime(started).TotalMilliseconds}ms";
+ Log.ExecutionResult(_logger, subscribe.Id, enumerator.Current, elapsed);
await Client.Next(new Next() { Id = subscribe.Id, Payload = enumerator.Current }, cancellationToken);
+ started = Stopwatch.GetTimestamp();
}
if (!cancellationToken.IsCancellationRequested)
{
await Client.Complete(new Complete() { Id = subscribe.Id }, cancellationToken);
}
+
+ Log.Completed(_logger, subscribe.Id, count);
}
catch (OperationCanceledException)
{
@@ -141,4 +157,16 @@ public async Task Complete(Complete complete, CancellationToken cancellationToke
await worker;
}
}
+
+ private static partial class Log
+ {
+ [LoggerMessage(5, LogLevel.Debug, "Subscription({Id}) - Result({elapsed}): {result}")]
+ public static partial void ExecutionResult(ILogger logger, string id, ExecutionResult? result, string elapsed);
+
+ [LoggerMessage(3, LogLevel.Debug, "Subscription({Id}) - Request: {request}")]
+ public static partial void Request(ILogger logger, string id, GraphQLRequest request);
+
+ [LoggerMessage(10, LogLevel.Information, "Subscription({Id}) - Server stream completed. {count} messages sent.")]
+ public static partial void Completed(ILogger logger, string id, ulong count);
+ }
}
\ No newline at end of file
diff --git a/src/GraphQL/DefaultOperationPipelineBuilderExtensions.cs b/src/GraphQL/DefaultOperationPipelineBuilderExtensions.cs
index 6d14af519..0ca737722 100644
--- a/src/GraphQL/DefaultOperationPipelineBuilderExtensions.cs
+++ b/src/GraphQL/DefaultOperationPipelineBuilderExtensions.cs
@@ -94,7 +94,7 @@ public static OperationDelegateBuilder UseDefaults(this OperationDelegateBuilder
public static OperationDelegateBuilder UseDefaultValidator(this OperationDelegateBuilder builder)
{
- var validator = new Validator3(ExecutionRules.All);
+ var validator = builder.ApplicationServices.GetRequiredService();
builder.Use(next => async context =>
{
diff --git a/src/GraphQL/Executor.cs b/src/GraphQL/Executor.cs
index fb98f11a7..431049d00 100644
--- a/src/GraphQL/Executor.cs
+++ b/src/GraphQL/Executor.cs
@@ -11,8 +11,6 @@ namespace Tanka.GraphQL;
///
public partial class Executor
{
- private static readonly IServiceProvider EmptyProvider = new ServiceCollection().BuildServiceProvider();
-
private readonly OperationDelegate _operationDelegate;
///
@@ -29,7 +27,9 @@ public Executor(ISchema schema) : this(new ExecutorOptions { Schema = schema })
///
public Executor(ExecutorOptions options)
{
- OperationDelegateBuilder builder = new(options.ServiceProvider ?? EmptyProvider);
+ OperationDelegateBuilder builder = new(options.ServiceProvider ?? new ServiceCollection()
+ .AddDefaultTankaGraphQLServices()
+ .BuildServiceProvider());
if (options.TraceEnabled)
builder.UseTrace();
diff --git a/src/GraphQL/ServiceCollectionExtensions.cs b/src/GraphQL/ServiceCollectionExtensions.cs
new file mode 100644
index 000000000..7589a7150
--- /dev/null
+++ b/src/GraphQL/ServiceCollectionExtensions.cs
@@ -0,0 +1,31 @@
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using Microsoft.Extensions.Options;
+
+using Tanka.GraphQL.Validation;
+
+namespace Tanka.GraphQL;
+
+public static class ServiceCollectionExtensions
+{
+ public static IServiceCollection AddDefaultTankaGraphQLServices(this IServiceCollection services)
+ {
+ return services.AddDefaultValidator();
+ }
+
+ public static IServiceCollection AddDefaultValidator(this IServiceCollection services)
+ {
+ services.AddOptions();
+ services.AddOptions();
+ services.TryAddSingleton(p =>
+ new AsyncValidator(p.GetRequiredService>()));
+
+ return services;
+ }
+
+ public static IServiceCollection AddDefaultValidatorRule(this IServiceCollection services, CombineRule rule)
+ {
+ services.PostConfigure(options => options.Rules.Add(rule));
+ return services;
+ }
+}
\ No newline at end of file
diff --git a/src/GraphQL/TypeSystem/SchemaBuilder.cs b/src/GraphQL/TypeSystem/SchemaBuilder.cs
index 599f766d4..27c093d8c 100644
--- a/src/GraphQL/TypeSystem/SchemaBuilder.cs
+++ b/src/GraphQL/TypeSystem/SchemaBuilder.cs
@@ -153,40 +153,48 @@ public SchemaBuilder Add(SchemaExtension schemaExtension)
/// Add type definition into the builder
///
///
- public void Add(TypeDefinition typeDefinition)
+ public SchemaBuilder Add(TypeDefinition typeDefinition)
{
if (!_typeDefinitions.TryAdd(typeDefinition.Name, typeDefinition))
throw TypeAlreadyExists(typeDefinition.Name);
+
+ return this;
}
///
/// Add type definitions into the builder
///
///
- public void Add(TypeDefinition[] typeDefinitions)
+ public SchemaBuilder Add(TypeDefinition[] typeDefinitions)
{
foreach (var typeDefinition in typeDefinitions)
Add(typeDefinition);
+
+ return this;
}
///
/// Add directive definition into the builder
///
///
- public void Add(DirectiveDefinition directiveDefinition)
+ public SchemaBuilder Add(DirectiveDefinition directiveDefinition)
{
if (!_directiveDefinitions.TryAdd(directiveDefinition.Name, directiveDefinition))
throw TypeAlreadyExists(directiveDefinition.Name);
+
+ return this;
}
///
/// Add directive definitions into the builder
///
///
- public void Add(DirectiveDefinition[] directiveDefinitions)
+ public SchemaBuilder Add(DirectiveDefinition[] directiveDefinitions)
{
foreach (var directiveDefinition in directiveDefinitions)
Add(directiveDefinition);
+
+ return this;
}
///
diff --git a/src/GraphQL/TypeSystem/TypeExtensions.cs b/src/GraphQL/TypeSystem/TypeExtensions.cs
index e72748c45..d24a14a12 100644
--- a/src/GraphQL/TypeSystem/TypeExtensions.cs
+++ b/src/GraphQL/TypeSystem/TypeExtensions.cs
@@ -8,7 +8,7 @@ public static NamedType Unwrap(this TypeBase type)
{
return type switch
{
- NonNullType NonNullType => Unwrap(NonNullType.OfType),
+ NonNullType nonNullType => Unwrap(nonNullType.OfType),
ListType list => Unwrap(list.OfType),
_ => (NamedType)type
};
diff --git a/src/GraphQL/Validation/ExecutionRules.cs b/src/GraphQL/Validation/ExecutionRules.cs
index 1e56873a4..8239ec834 100644
--- a/src/GraphQL/Validation/ExecutionRules.cs
+++ b/src/GraphQL/Validation/ExecutionRules.cs
@@ -1,6 +1,7 @@
using Tanka.GraphQL.Language;
using Tanka.GraphQL.Language.Nodes;
using Tanka.GraphQL.Language.Nodes.TypeSystem;
+using Tanka.GraphQL.Request;
using Tanka.GraphQL.SelectionSets;
using Tanka.GraphQL.ValueSerialization;
@@ -406,8 +407,42 @@ void ValidateArguments(
}
// variables should be valid
- if (argument.Value is Variable)
- continue;
+ if (argument.Value is Variable variable)
+ {
+ if (ruleVisitorContext.VariableValues is null)
+ {
+ ruleVisitorContext.Error(
+ ValidationErrorCodes.R5421RequiredArguments,
+ "Arguments is required. An argument is required " +
+ "if the argument type is non‐null and does not have a default " +
+ "value. Otherwise, the argument is optional. " +
+ $"Value of argument '{argumentName}' cannot be null");
+
+ return;
+ }
+
+
+ if (!ruleVisitorContext.VariableValues.TryGetValue(variable.Name, out var variableValue))
+ ruleVisitorContext.Error(
+ ValidationErrorCodes.R5421RequiredArguments,
+ "Arguments is required. An argument is required " +
+ "if the argument type is non‐null and does not have a default " +
+ "value. Otherwise, the argument is optional. " +
+ $"Value of argument '{argumentName}' cannot be null");
+ else
+ {
+ if (variableValue is null)
+ {
+ ruleVisitorContext.Error(
+ ValidationErrorCodes.R5421RequiredArguments,
+ "Arguments is required. An argument is required " +
+ "if the argument type is non‐null and does not have a default " +
+ "value. Otherwise, the argument is optional. " +
+ $"Value of argument '{argumentName}' cannot be null");
+ }
+ }
+
+ }
if (argument?.Value == null || argument.Value.Kind == NodeKind.NullValue)
ruleVisitorContext.Error(
@@ -864,7 +899,7 @@ public static CombineRule R561ValuesOfCorrectType()
return;
}
- var fieldNodeMap = node.Fields.ToDictionary(
+ var fieldNodeMap = node.ToDictionary(
f => f.Name);
foreach (var fieldDef in context.Schema.GetInputFields(
@@ -1034,7 +1069,7 @@ public static CombineRule R563InputObjectFieldUniqueness()
{
rule.EnterObjectValue += node =>
{
- var fields = node.Fields.ToList();
+ var fields = node.ToList();
foreach (var inputField in fields)
{
@@ -1063,7 +1098,7 @@ public static CombineRule R564InputObjectRequiredFields()
if (inputObject == null)
return;
- var fields = node.Fields.ToDictionary(f => f.Name);
+ var fields = node.ToDictionary(f => f.Name);
var fieldDefinitions = context.Schema.GetInputFields(inputObject.Name);
foreach (var fieldDefinition in fieldDefinitions)
diff --git a/src/GraphQL/Validation/Validator.cs b/src/GraphQL/Validation/Validator.cs
index 55ce15d50..22fd92ff1 100644
--- a/src/GraphQL/Validation/Validator.cs
+++ b/src/GraphQL/Validation/Validator.cs
@@ -1,8 +1,10 @@
+using Microsoft.Extensions.Options;
+
using Tanka.GraphQL.Language.Nodes;
namespace Tanka.GraphQL.Validation;
-public interface IValidator3
+public interface IAsyncValidator
{
ValueTask Validate(
ISchema schema,
@@ -10,14 +12,14 @@ ValueTask Validate(
IReadOnlyDictionary? variables);
}
-public class Validator3 : IValidator3
+public class AsyncValidatorOptions
{
- private readonly IEnumerable _rules;
+ public List Rules { get; set; } = [.. ExecutionRules.All];
+}
- public Validator3(IEnumerable rules)
- {
- _rules = rules;
- }
+public class AsyncValidator : IAsyncValidator
+{
+ private readonly IOptions _optionsMonitor;
public ValueTask Validate(
ISchema schema,
@@ -25,15 +27,25 @@ public ValueTask Validate(
IReadOnlyDictionary? variables)
{
var visitor = new RulesWalker(
- _rules,
+ _optionsMonitor.Value.Rules,
schema,
document,
variables);
return new(visitor.Validate());
}
+
+ public AsyncValidator(IEnumerable rules) : this(Options.Create(new AsyncValidatorOptions() { Rules = [..rules]}))
+ {
+ }
+
+ public AsyncValidator(IOptions optionsMonitor)
+ {
+ _optionsMonitor = optionsMonitor;
+ }
}
+[Obsolete("Use AsyncValidator")]
public static class Validator
{
public static ValidationResult Validate(
diff --git a/src/GraphQL/Validation/ValidatorFeature.cs b/src/GraphQL/Validation/ValidatorFeature.cs
index 3a61e7aa1..17bdf2796 100644
--- a/src/GraphQL/Validation/ValidatorFeature.cs
+++ b/src/GraphQL/Validation/ValidatorFeature.cs
@@ -5,7 +5,7 @@ namespace Tanka.GraphQL.Validation;
public class ValidatorFeature : IValidatorFeature
{
- public IValidator3? Validator { get; set; }
+ public IAsyncValidator? Validator { get; set; }
public ValueTask Validate(
ISchema schema,
diff --git a/src/GraphQL/Validation/Visitor.cs b/src/GraphQL/Validation/Visitor.cs
index 469f49879..58c7d6556 100644
--- a/src/GraphQL/Validation/Visitor.cs
+++ b/src/GraphQL/Validation/Visitor.cs
@@ -245,7 +245,7 @@ public virtual void Visit(ExecutableDocument ast)
public virtual ObjectValue BeginVisitObjectValue(
ObjectValue node)
{
- foreach (var objectField in node.Fields) BeginVisitNode(objectField);
+ foreach (var objectField in node) BeginVisitNode(objectField);
return EndVisitObjectValue(node);
}
diff --git a/src/GraphQL/Values.cs b/src/GraphQL/Values.cs
index 57b8e9c94..7e23f5b5f 100644
--- a/src/GraphQL/Values.cs
+++ b/src/GraphQL/Values.cs
@@ -90,7 +90,7 @@ public static class Values
Dictionary result)
{
var fields = schema.GetInputFields(input.Name);
- var valueFields = objectValue.Fields.ToDictionary(f => f.Name.Value, f => f);
+ var valueFields = objectValue.ToDictionary(f => f.Name.Value, f => f);
foreach (var inputField in fields)
{
diff --git a/tanka-graphql.sln b/tanka-graphql.sln
index 8684098e1..fe59c7712 100644
--- a/tanka-graphql.sln
+++ b/tanka-graphql.sln
@@ -74,6 +74,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GraphQL.Samples.SG.Services
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GraphQL.Samples.SG.Arguments", "samples\GraphQL.Samples.SG.Arguments\GraphQL.Samples.SG.Arguments.csproj", "{4A12194D-8289-462C-94B8-8ABDE2D8283A}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GraphQL.Extensions.Experimental.Tests", "tests\GraphQL.Extensions.Experimental.Tests\GraphQL.Extensions.Experimental.Tests.csproj", "{4F85C0B5-2F59-46AE-993E-4719B2C954B6}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GraphQL.Extensions.Experimental", "src\GraphQL.Extensions.Experimental\GraphQL.Extensions.Experimental.csproj", "{35D039EE-6718-43E2-83CD-00ACA4644FB0}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -360,6 +364,30 @@ Global
{4A12194D-8289-462C-94B8-8ABDE2D8283A}.Release|x64.Build.0 = Release|Any CPU
{4A12194D-8289-462C-94B8-8ABDE2D8283A}.Release|x86.ActiveCfg = Release|Any CPU
{4A12194D-8289-462C-94B8-8ABDE2D8283A}.Release|x86.Build.0 = Release|Any CPU
+ {4F85C0B5-2F59-46AE-993E-4719B2C954B6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {4F85C0B5-2F59-46AE-993E-4719B2C954B6}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {4F85C0B5-2F59-46AE-993E-4719B2C954B6}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {4F85C0B5-2F59-46AE-993E-4719B2C954B6}.Debug|x64.Build.0 = Debug|Any CPU
+ {4F85C0B5-2F59-46AE-993E-4719B2C954B6}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {4F85C0B5-2F59-46AE-993E-4719B2C954B6}.Debug|x86.Build.0 = Debug|Any CPU
+ {4F85C0B5-2F59-46AE-993E-4719B2C954B6}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {4F85C0B5-2F59-46AE-993E-4719B2C954B6}.Release|Any CPU.Build.0 = Release|Any CPU
+ {4F85C0B5-2F59-46AE-993E-4719B2C954B6}.Release|x64.ActiveCfg = Release|Any CPU
+ {4F85C0B5-2F59-46AE-993E-4719B2C954B6}.Release|x64.Build.0 = Release|Any CPU
+ {4F85C0B5-2F59-46AE-993E-4719B2C954B6}.Release|x86.ActiveCfg = Release|Any CPU
+ {4F85C0B5-2F59-46AE-993E-4719B2C954B6}.Release|x86.Build.0 = Release|Any CPU
+ {35D039EE-6718-43E2-83CD-00ACA4644FB0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {35D039EE-6718-43E2-83CD-00ACA4644FB0}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {35D039EE-6718-43E2-83CD-00ACA4644FB0}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {35D039EE-6718-43E2-83CD-00ACA4644FB0}.Debug|x64.Build.0 = Debug|Any CPU
+ {35D039EE-6718-43E2-83CD-00ACA4644FB0}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {35D039EE-6718-43E2-83CD-00ACA4644FB0}.Debug|x86.Build.0 = Debug|Any CPU
+ {35D039EE-6718-43E2-83CD-00ACA4644FB0}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {35D039EE-6718-43E2-83CD-00ACA4644FB0}.Release|Any CPU.Build.0 = Release|Any CPU
+ {35D039EE-6718-43E2-83CD-00ACA4644FB0}.Release|x64.ActiveCfg = Release|Any CPU
+ {35D039EE-6718-43E2-83CD-00ACA4644FB0}.Release|x64.Build.0 = Release|Any CPU
+ {35D039EE-6718-43E2-83CD-00ACA4644FB0}.Release|x86.ActiveCfg = Release|Any CPU
+ {35D039EE-6718-43E2-83CD-00ACA4644FB0}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/tanka-graphql.sln.DotSettings b/tanka-graphql.sln.DotSettings
index 188dd4195..f8787e29f 100644
--- a/tanka-graphql.sln.DotSettings
+++ b/tanka-graphql.sln.DotSettings
@@ -6,6 +6,7 @@
True
True
True
+ True
True
True
True
diff --git a/tests/GraphQL.Extensions.Experimental.Tests/GlobalUsings.cs b/tests/GraphQL.Extensions.Experimental.Tests/GlobalUsings.cs
new file mode 100644
index 000000000..8c927eb74
--- /dev/null
+++ b/tests/GraphQL.Extensions.Experimental.Tests/GlobalUsings.cs
@@ -0,0 +1 @@
+global using Xunit;
\ No newline at end of file
diff --git a/tests/GraphQL.Extensions.Experimental.Tests/GraphQL.Extensions.Experimental.Tests.csproj b/tests/GraphQL.Extensions.Experimental.Tests/GraphQL.Extensions.Experimental.Tests.csproj
new file mode 100644
index 000000000..1200debea
--- /dev/null
+++ b/tests/GraphQL.Extensions.Experimental.Tests/GraphQL.Extensions.Experimental.Tests.csproj
@@ -0,0 +1,31 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+ false
+ true
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/GraphQL.Extensions.Experimental.Tests/OneOf/ValidationRuleFacts.cs b/tests/GraphQL.Extensions.Experimental.Tests/OneOf/ValidationRuleFacts.cs
new file mode 100644
index 000000000..2485e7d2c
--- /dev/null
+++ b/tests/GraphQL.Extensions.Experimental.Tests/OneOf/ValidationRuleFacts.cs
@@ -0,0 +1,212 @@
+using Tanka.GraphQL.Extensions.Experimental.OneOf;
+using Tanka.GraphQL.TypeSystem;
+using Tanka.GraphQL.Validation;
+
+namespace Tanka.GraphQL.Extensions.Experimental.Tests.OneOf;
+
+public class ValidationRuleFacts
+{
+ [Fact]
+ public async Task Valid_when_one_field_set()
+ {
+ /* Given */
+ ISchema schema = await new SchemaBuilder()
+ .Add(OneOfDirective.Directive)
+ .Add("""
+ input OneOfInput @oneOf {
+ a: String
+ b: String
+ }
+
+ type Query {
+ oneOf(input: OneOfInput!): String
+ }
+ """)
+ .Build(new SchemaBuildOptions());
+
+ var validator = new AsyncValidator([..ExecutionRules.All, OneOfDirective.OneOfValidationRule()]);
+
+ /* When */
+ ValidationResult result = await validator.Validate(schema, """
+ {
+ oneOf(input: { a: "a" })
+ }
+ """,
+ new Dictionary()
+ );
+
+ /* Then */
+ Assert.True(result.IsValid);
+ }
+
+ [Fact]
+ public async Task Valid_when_one_field_set_for_nullable()
+ {
+ /* Given */
+ ISchema schema = await new SchemaBuilder()
+ .Add(OneOfDirective.Directive)
+ .Add("""
+ input OneOfInput @oneOf {
+ a: String
+ b: String
+ }
+
+ type Query {
+ oneOf(input: OneOfInput): String
+ }
+ """)
+ .Build(new SchemaBuildOptions());
+
+ var validator = new AsyncValidator([.. ExecutionRules.All, OneOfDirective.OneOfValidationRule()]);
+
+ /* When */
+ ValidationResult result = await validator.Validate(schema, """
+ {
+ oneOf(input: { a: "a" })
+ }
+ """,
+ new Dictionary()
+ );
+
+ /* Then */
+ Assert.True(result.IsValid);
+ }
+
+ [Fact]
+ public async Task Invalid_when_two_field_set()
+ {
+ /* Given */
+ ISchema schema = await new SchemaBuilder()
+ .Add(OneOfDirective.Directive)
+ .Add("""
+ input OneOfInput @oneOf {
+ a: String
+ b: String
+ }
+
+ type Query {
+ oneOf(input: OneOfInput!): String
+ }
+ """)
+ .Build(new SchemaBuildOptions());
+
+ var validator = new AsyncValidator([.. ExecutionRules.All, OneOfDirective.OneOfValidationRule()]);
+
+ /* When */
+ ValidationResult result = await validator.Validate(schema, """
+ {
+ oneOf(input: { a: "a", b: "b" })
+ }
+ """,
+ new Dictionary()
+ );
+
+ /* Then */
+ Assert.False(result.IsValid);
+ Assert.Equal("ONEOF001", result.Errors.Single().Code);
+ }
+
+ [Fact]
+ public async Task Invalid_when_two_field_set_for_nullable()
+ {
+ /* Given */
+ ISchema schema = await new SchemaBuilder()
+ .Add(OneOfDirective.Directive)
+ .Add("""
+ input OneOfInput @oneOf {
+ a: String
+ b: String
+ }
+
+ type Query {
+ oneOf(input: OneOfInput): String
+ }
+ """)
+ .Build(new SchemaBuildOptions());
+
+ var validator = new AsyncValidator([.. ExecutionRules.All, OneOfDirective.OneOfValidationRule()]);
+
+ /* When */
+ ValidationResult result = await validator.Validate(schema, """
+ {
+ oneOf(input: { a: "a", b: "b" })
+ }
+ """,
+ new Dictionary()
+ );
+
+ /* Then */
+ Assert.False(result.IsValid);
+ Assert.Equal("ONEOF001", result.Errors.Single().Code);
+ }
+
+ [Fact]
+ public async Task Valid_when_one_field_set_as_variable()
+ {
+ /* Given */
+ ISchema schema = await new SchemaBuilder()
+ .Add(OneOfDirective.Directive)
+ .Add("""
+ input OneOfInput @oneOf {
+ a: String
+ b: String
+ }
+
+ type Query {
+ oneOf(input: OneOfInput!): String
+ }
+ """)
+ .Build(new SchemaBuildOptions());
+
+ var validator = new AsyncValidator([.. ExecutionRules.All, OneOfDirective.OneOfValidationRule()]);
+
+ /* When */
+ ValidationResult result = await validator.Validate(schema, """
+ query ($variable: OneOfInput!) {
+ oneOf(input: $variable)
+ }
+ """,
+ new Dictionary { ["variable"] = new Dictionary { ["a"] = "a" } }
+ );
+
+ /* Then */
+ Assert.True(result.IsValid);
+ }
+
+ [Fact]
+ public async Task Invalid_when_two_field_set_as_variable()
+ {
+ /* Given */
+ ISchema schema = await new SchemaBuilder()
+ .Add(OneOfDirective.Directive)
+ .Add("""
+ input OneOfInput @oneOf {
+ a: String
+ b: String
+ }
+
+ type Query {
+ oneOf(input: OneOfInput!): String
+ }
+ """)
+ .Build(new SchemaBuildOptions());
+
+ var validator = new AsyncValidator([.. ExecutionRules.All, OneOfDirective.OneOfValidationRule()]);
+
+ /* When */
+ ValidationResult result = await validator.Validate(schema, """
+ query ($variable: OneOfInput!) {
+ oneOf(input: $variable)
+ }
+ """,
+ new Dictionary
+ {
+ ["variable"] = new Dictionary { ["a"] = "a", ["b"] = "b" }
+ }
+ );
+
+ /* Then */
+ Assert.False(result.IsValid);
+ Assert.Equal("ONEOF001", result.Errors.Single().Code);
+ }
+}
\ No newline at end of file
diff --git a/tests/GraphQL.Language.Tests/ParserFacts.cs b/tests/GraphQL.Language.Tests/ParserFacts.cs
index 366153ae7..b123cbd80 100644
--- a/tests/GraphQL.Language.Tests/ParserFacts.cs
+++ b/tests/GraphQL.Language.Tests/ParserFacts.cs
@@ -856,7 +856,7 @@ public void Value_ObjectValue_Empty()
/* Then */
var listValue = Assert.IsType(value);
- Assert.Equal(0, listValue.Fields.Count);
+ Assert.Equal(0, listValue.Count);
}
[Fact]
@@ -870,7 +870,7 @@ public void Value_ObjectValue_with_Fields()
/* Then */
var listValue = Assert.IsType(value);
- Assert.Equal(2, listValue.Fields.Count);
+ Assert.Equal(2, listValue.Count);
}
[Theory]
diff --git a/tutorials/GraphQL.Tutorials.Getting-Started/GettingStarted.cs b/tutorials/GraphQL.Tutorials.Getting-Started/GettingStarted.cs
index 0bb6a3380..2bdf1831c 100644
--- a/tutorials/GraphQL.Tutorials.Getting-Started/GettingStarted.cs
+++ b/tutorials/GraphQL.Tutorials.Getting-Started/GettingStarted.cs
@@ -6,7 +6,9 @@
using Tanka.GraphQL.Directives;
using Tanka.GraphQL.Language.Nodes;
using Tanka.GraphQL.Request;
+using Tanka.GraphQL.Server;
using Tanka.GraphQL.TypeSystem;
+using Tanka.GraphQL.Validation;
using Tanka.GraphQL.ValueResolution;
using Tanka.GraphQL.ValueSerialization;
using Xunit;
@@ -322,6 +324,7 @@ type Query {
{
Schema = schema,
ServiceProvider = new ServiceCollection()
+ .AddDefaultTankaGraphQLServices()
.AddSingleton()
.BuildServiceProvider()
}).Execute(new GraphQLRequest("{name}"));
@@ -362,6 +365,7 @@ type Query {
{
Schema = schema,
ServiceProvider = new ServiceCollection()
+ .AddDefaultTankaGraphQLServices()
.AddSingleton()
.BuildServiceProvider()
}).Execute(new GraphQLRequest("{name}"));