diff --git a/samples/Samples.Schemas.Chat/ChatMutation.cs b/samples/Samples.Schemas.Chat/ChatMutation.cs index 0b069085..e23e1771 100644 --- a/samples/Samples.Schemas.Chat/ChatMutation.cs +++ b/samples/Samples.Schemas.Chat/ChatMutation.cs @@ -1,31 +1,30 @@ using GraphQL.Types; -namespace GraphQL.Samples.Schemas.Chat +namespace GraphQL.Samples.Schemas.Chat; + +public class ChatMutation : ObjectGraphType { - public class ChatMutation : ObjectGraphType + public ChatMutation(IChat chat) { - public ChatMutation(IChat chat) - { - Field("addMessage", - arguments: new QueryArguments( - new QueryArgument { Name = "message" } - ), - resolve: context => - { - var receivedMessage = context.GetArgument("message"); - var message = chat.AddMessage(receivedMessage); - return message; - }); - } + Field("addMessage", + arguments: new QueryArguments( + new QueryArgument { Name = "message" } + ), + resolve: context => + { + var receivedMessage = context.GetArgument("message"); + var message = chat.AddMessage(receivedMessage); + return message; + }); } +} - public class MessageInputType : InputObjectGraphType +public class MessageInputType : InputObjectGraphType +{ + public MessageInputType() { - public MessageInputType() - { - Field("fromId"); - Field("content"); - Field("sentAt"); - } + Field("fromId"); + Field("content"); + Field("sentAt"); } } diff --git a/samples/Samples.Schemas.Chat/ChatQuery.cs b/samples/Samples.Schemas.Chat/ChatQuery.cs index ed77ef9a..30aa06da 100644 --- a/samples/Samples.Schemas.Chat/ChatQuery.cs +++ b/samples/Samples.Schemas.Chat/ChatQuery.cs @@ -1,12 +1,11 @@ using GraphQL.Types; -namespace GraphQL.Samples.Schemas.Chat +namespace GraphQL.Samples.Schemas.Chat; + +public class ChatQuery : ObjectGraphType { - public class ChatQuery : ObjectGraphType + public ChatQuery(IChat chat) { - public ChatQuery(IChat chat) - { - Field>("messages", resolve: context => chat.AllMessages.Take(100)); - } + Field>("messages", resolve: context => chat.AllMessages.Take(100)); } } diff --git a/samples/Samples.Schemas.Chat/ChatSchema.cs b/samples/Samples.Schemas.Chat/ChatSchema.cs index a67ee65a..149e75e9 100644 --- a/samples/Samples.Schemas.Chat/ChatSchema.cs +++ b/samples/Samples.Schemas.Chat/ChatSchema.cs @@ -1,14 +1,13 @@ using GraphQL.Types; -namespace GraphQL.Samples.Schemas.Chat +namespace GraphQL.Samples.Schemas.Chat; + +public class ChatSchema : Schema { - public class ChatSchema : Schema + public ChatSchema(IChat chat, IServiceProvider provider) : base(provider) { - public ChatSchema(IChat chat, IServiceProvider provider) : base(provider) - { - Query = new ChatQuery(chat); - Mutation = new ChatMutation(chat); - Subscription = new ChatSubscriptions(chat); - } + Query = new ChatQuery(chat); + Mutation = new ChatMutation(chat); + Subscription = new ChatSubscriptions(chat); } } diff --git a/samples/Samples.Schemas.Chat/ChatSubscriptions.cs b/samples/Samples.Schemas.Chat/ChatSubscriptions.cs index d6788660..f82b67dc 100644 --- a/samples/Samples.Schemas.Chat/ChatSubscriptions.cs +++ b/samples/Samples.Schemas.Chat/ChatSubscriptions.cs @@ -4,67 +4,66 @@ using GraphQL.Server.Transports.Subscriptions.Abstractions; using GraphQL.Types; -namespace GraphQL.Samples.Schemas.Chat +namespace GraphQL.Samples.Schemas.Chat; + +public class ChatSubscriptions : ObjectGraphType { - public class ChatSubscriptions : ObjectGraphType - { - private readonly IChat _chat; + private readonly IChat _chat; - public ChatSubscriptions(IChat chat) + public ChatSubscriptions(IChat chat) + { + _chat = chat; + AddField(new FieldType { - _chat = chat; - AddField(new FieldType - { - Name = "messageAdded", - Type = typeof(MessageType), - Resolver = new FuncFieldResolver(ResolveMessage), - StreamResolver = new SourceStreamResolver(Subscribe) - }); - - AddField(new FieldType - { - Name = "messageAddedByUser", - Arguments = new QueryArguments( - new QueryArgument> { Name = "id" } - ), - Type = typeof(MessageType), - Resolver = new FuncFieldResolver(ResolveMessage), - StreamResolver = new SourceStreamResolver(SubscribeById) - }); - } + Name = "messageAdded", + Type = typeof(MessageType), + Resolver = new FuncFieldResolver(ResolveMessage), + StreamResolver = new SourceStreamResolver(Subscribe) + }); - private IObservable SubscribeById(IResolveFieldContext context) + AddField(new FieldType { - var messageContext = (MessageHandlingContext)context.UserContext; - var user = messageContext.Get("user"); + Name = "messageAddedByUser", + Arguments = new QueryArguments( + new QueryArgument> { Name = "id" } + ), + Type = typeof(MessageType), + Resolver = new FuncFieldResolver(ResolveMessage), + StreamResolver = new SourceStreamResolver(SubscribeById) + }); + } - string sub = "Anonymous"; - if (user != null) - sub = user.Claims.FirstOrDefault(c => c.Type == "sub")?.Value; + private IObservable SubscribeById(IResolveFieldContext context) + { + var messageContext = (MessageHandlingContext)context.UserContext; + var user = messageContext.Get("user"); - var messages = _chat.Messages(sub); + string sub = "Anonymous"; + if (user != null) + sub = user.Claims.FirstOrDefault(c => c.Type == "sub")?.Value; - string id = context.GetArgument("id"); - return messages.Where(message => message.From.Id == id); - } + var messages = _chat.Messages(sub); - private Message ResolveMessage(IResolveFieldContext context) - { - var message = context.Source as Message; + string id = context.GetArgument("id"); + return messages.Where(message => message.From.Id == id); + } - return message; - } + private Message ResolveMessage(IResolveFieldContext context) + { + var message = context.Source as Message; - private IObservable Subscribe(IResolveFieldContext context) - { - var messageContext = (MessageHandlingContext)context.UserContext; - var user = messageContext.Get("user"); + return message; + } + + private IObservable Subscribe(IResolveFieldContext context) + { + var messageContext = (MessageHandlingContext)context.UserContext; + var user = messageContext.Get("user"); - string sub = "Anonymous"; - if (user != null) - sub = user.Claims.FirstOrDefault(c => c.Type == "sub")?.Value; + string sub = "Anonymous"; + if (user != null) + sub = user.Claims.FirstOrDefault(c => c.Type == "sub")?.Value; - return _chat.Messages(sub); - } + return _chat.Messages(sub); } } diff --git a/samples/Samples.Schemas.Chat/IChat.cs b/samples/Samples.Schemas.Chat/IChat.cs index 05470a6e..3f3324a7 100644 --- a/samples/Samples.Schemas.Chat/IChat.cs +++ b/samples/Samples.Schemas.Chat/IChat.cs @@ -2,74 +2,73 @@ using System.Reactive.Linq; using System.Reactive.Subjects; -namespace GraphQL.Samples.Schemas.Chat +namespace GraphQL.Samples.Schemas.Chat; + +public interface IChat { - public interface IChat - { - ConcurrentStack AllMessages { get; } + ConcurrentStack AllMessages { get; } - Message AddMessage(Message message); + Message AddMessage(Message message); - IObservable Messages(string user); + IObservable Messages(string user); - Message AddMessage(ReceivedMessage message); - } + Message AddMessage(ReceivedMessage message); +} - public class Chat : IChat - { - private readonly ISubject _messageStream = new ReplaySubject(1); +public class Chat : IChat +{ + private readonly ISubject _messageStream = new ReplaySubject(1); - public Chat() + public Chat() + { + AllMessages = new ConcurrentStack(); + Users = new ConcurrentDictionary { - AllMessages = new ConcurrentStack(); - Users = new ConcurrentDictionary - { - ["1"] = "developer", - ["2"] = "tester" - }; - } + ["1"] = "developer", + ["2"] = "tester" + }; + } - public ConcurrentDictionary Users { get; set; } + public ConcurrentDictionary Users { get; set; } - public ConcurrentStack AllMessages { get; } + public ConcurrentStack AllMessages { get; } - public Message AddMessage(ReceivedMessage message) + public Message AddMessage(ReceivedMessage message) + { + if (!Users.TryGetValue(message.FromId, out string displayName)) { - if (!Users.TryGetValue(message.FromId, out string displayName)) - { - displayName = "(unknown)"; - } - - return AddMessage(new Message - { - Content = message.Content, - SentAt = message.SentAt ?? DateTime.UtcNow, - From = new MessageFrom - { - DisplayName = displayName, - Id = message.FromId - } - }); + displayName = "(unknown)"; } - public Message AddMessage(Message message) + return AddMessage(new Message { - AllMessages.Push(message); - _messageStream.OnNext(message); - return message; - } + Content = message.Content, + SentAt = message.SentAt ?? DateTime.UtcNow, + From = new MessageFrom + { + DisplayName = displayName, + Id = message.FromId + } + }); + } - public IObservable Messages(string user) - { - return _messageStream - .Select(message => - { - message.Sub = user; - return message; - }) - .AsObservable(); - } + public Message AddMessage(Message message) + { + AllMessages.Push(message); + _messageStream.OnNext(message); + return message; + } - public void AddError(Exception exception) => _messageStream.OnError(exception); + public IObservable Messages(string user) + { + return _messageStream + .Select(message => + { + message.Sub = user; + return message; + }) + .AsObservable(); } + + public void AddError(Exception exception) => _messageStream.OnError(exception); } diff --git a/samples/Samples.Schemas.Chat/Message.cs b/samples/Samples.Schemas.Chat/Message.cs index 67d702a3..57f54147 100644 --- a/samples/Samples.Schemas.Chat/Message.cs +++ b/samples/Samples.Schemas.Chat/Message.cs @@ -1,13 +1,12 @@ -namespace GraphQL.Samples.Schemas.Chat +namespace GraphQL.Samples.Schemas.Chat; + +public class Message { - public class Message - { - public MessageFrom From { get; set; } + public MessageFrom From { get; set; } - public string Sub { get; set; } + public string Sub { get; set; } - public string Content { get; set; } + public string Content { get; set; } - public DateTime? SentAt { get; set; } - } + public DateTime? SentAt { get; set; } } diff --git a/samples/Samples.Schemas.Chat/MessageFrom.cs b/samples/Samples.Schemas.Chat/MessageFrom.cs index a3493ed5..7444b230 100644 --- a/samples/Samples.Schemas.Chat/MessageFrom.cs +++ b/samples/Samples.Schemas.Chat/MessageFrom.cs @@ -1,9 +1,8 @@ -namespace GraphQL.Samples.Schemas.Chat +namespace GraphQL.Samples.Schemas.Chat; + +public class MessageFrom { - public class MessageFrom - { - public string Id { get; set; } + public string Id { get; set; } - public string DisplayName { get; set; } - } + public string DisplayName { get; set; } } diff --git a/samples/Samples.Schemas.Chat/MessageFromType.cs b/samples/Samples.Schemas.Chat/MessageFromType.cs index c9dc59c2..e7616fc7 100644 --- a/samples/Samples.Schemas.Chat/MessageFromType.cs +++ b/samples/Samples.Schemas.Chat/MessageFromType.cs @@ -1,13 +1,12 @@ using GraphQL.Types; -namespace GraphQL.Samples.Schemas.Chat +namespace GraphQL.Samples.Schemas.Chat; + +public class MessageFromType : ObjectGraphType { - public class MessageFromType : ObjectGraphType + public MessageFromType() { - public MessageFromType() - { - Field(o => o.Id); - Field(o => o.DisplayName); - } + Field(o => o.Id); + Field(o => o.DisplayName); } } diff --git a/samples/Samples.Schemas.Chat/MessageType.cs b/samples/Samples.Schemas.Chat/MessageType.cs index 8d157c98..c7705663 100644 --- a/samples/Samples.Schemas.Chat/MessageType.cs +++ b/samples/Samples.Schemas.Chat/MessageType.cs @@ -1,21 +1,20 @@ using GraphQL.Types; -namespace GraphQL.Samples.Schemas.Chat +namespace GraphQL.Samples.Schemas.Chat; + +public class MessageType : ObjectGraphType { - public class MessageType : ObjectGraphType + public MessageType() { - public MessageType() - { - Field(o => o.Content); - Field(o => o.SentAt, type: typeof(DateTimeGraphType)); - Field(o => o.Sub); - Field(o => o.From, false, typeof(MessageFromType)).Resolve(ResolveFrom); - } + Field(o => o.Content); + Field(o => o.SentAt, type: typeof(DateTimeGraphType)); + Field(o => o.Sub); + Field(o => o.From, false, typeof(MessageFromType)).Resolve(ResolveFrom); + } - private MessageFrom ResolveFrom(IResolveFieldContext context) - { - var message = context.Source; - return message.From; - } + private MessageFrom ResolveFrom(IResolveFieldContext context) + { + var message = context.Source; + return message.From; } } diff --git a/samples/Samples.Schemas.Chat/ReceivedMessage.cs b/samples/Samples.Schemas.Chat/ReceivedMessage.cs index 63646360..edbd1c5c 100644 --- a/samples/Samples.Schemas.Chat/ReceivedMessage.cs +++ b/samples/Samples.Schemas.Chat/ReceivedMessage.cs @@ -1,11 +1,10 @@ -namespace GraphQL.Samples.Schemas.Chat +namespace GraphQL.Samples.Schemas.Chat; + +public class ReceivedMessage { - public class ReceivedMessage - { - public string FromId { get; set; } + public string FromId { get; set; } - public string Content { get; set; } + public string Content { get; set; } - public DateTime? SentAt { get; set; } - } + public DateTime? SentAt { get; set; } } diff --git a/samples/Samples.Server/CustomErrorInfoProvider.cs b/samples/Samples.Server/CustomErrorInfoProvider.cs index d5462469..533750ea 100644 --- a/samples/Samples.Server/CustomErrorInfoProvider.cs +++ b/samples/Samples.Server/CustomErrorInfoProvider.cs @@ -3,54 +3,53 @@ using GraphQL.Server.Authorization.AspNetCore; using Microsoft.AspNetCore.Authorization; -namespace GraphQL.Samples.Server +namespace GraphQL.Samples.Server; + +/// +/// Custom implementing a dedicated error message for the sample +/// used in this MS article: https://docs.microsoft.com/en-us/aspnet/core/security/authorization/policies +/// +public class CustomErrorInfoProvider : ErrorInfoProvider { - /// - /// Custom implementing a dedicated error message for the sample - /// used in this MS article: https://docs.microsoft.com/en-us/aspnet/core/security/authorization/policies - /// - public class CustomErrorInfoProvider : ErrorInfoProvider + private readonly IAuthorizationErrorMessageBuilder _messageBuilder; + + public CustomErrorInfoProvider(IAuthorizationErrorMessageBuilder messageBuilder) { - private readonly IAuthorizationErrorMessageBuilder _messageBuilder; + _messageBuilder = messageBuilder; + } - public CustomErrorInfoProvider(IAuthorizationErrorMessageBuilder messageBuilder) + public override ErrorInfo GetInfo(ExecutionError executionError) + { + var info = base.GetInfo(executionError); + info.Message = executionError switch { - _messageBuilder = messageBuilder; - } + AuthorizationError authorizationError => GetAuthorizationErrorMessage(authorizationError), + _ => info.Message, + }; + return info; + } - public override ErrorInfo GetInfo(ExecutionError executionError) - { - var info = base.GetInfo(executionError); - info.Message = executionError switch - { - AuthorizationError authorizationError => GetAuthorizationErrorMessage(authorizationError), - _ => info.Message, - }; - return info; - } + private string GetAuthorizationErrorMessage(AuthorizationError error) + { + var errorMessage = new StringBuilder(); + _messageBuilder.AppendFailureHeader(errorMessage, error.OperationType); - private string GetAuthorizationErrorMessage(AuthorizationError error) + foreach (var failedRequirement in error.AuthorizationResult.Failure.FailedRequirements) { - var errorMessage = new StringBuilder(); - _messageBuilder.AppendFailureHeader(errorMessage, error.OperationType); - - foreach (var failedRequirement in error.AuthorizationResult.Failure.FailedRequirements) + switch (failedRequirement) { - switch (failedRequirement) - { - case MinimumAgeRequirement minimumAgeRequirement: - errorMessage.AppendLine(); - errorMessage.Append("The current user must be at least "); - errorMessage.Append(minimumAgeRequirement.MinimumAge); - errorMessage.Append(" years old."); - break; - default: - _messageBuilder.AppendFailureLine(errorMessage, failedRequirement); - break; - } + case MinimumAgeRequirement minimumAgeRequirement: + errorMessage.AppendLine(); + errorMessage.Append("The current user must be at least "); + errorMessage.Append(minimumAgeRequirement.MinimumAge); + errorMessage.Append(" years old."); + break; + default: + _messageBuilder.AppendFailureLine(errorMessage, failedRequirement); + break; } - - return errorMessage.ToString(); } + + return errorMessage.ToString(); } } diff --git a/samples/Samples.Server/GraphQLHttpMiddlewareWithLogs.cs b/samples/Samples.Server/GraphQLHttpMiddlewareWithLogs.cs index 5b8565da..e2d752fc 100644 --- a/samples/Samples.Server/GraphQLHttpMiddlewareWithLogs.cs +++ b/samples/Samples.Server/GraphQLHttpMiddlewareWithLogs.cs @@ -1,42 +1,41 @@ using GraphQL.Server.Transports.AspNetCore; using GraphQL.Types; -namespace GraphQL.Samples.Server +namespace GraphQL.Samples.Server; + +// Example of a custom GraphQL Middleware that sends execution result to Microsoft.Extensions.Logging API +public class GraphQLHttpMiddlewareWithLogs : GraphQLHttpMiddleware + where TSchema : ISchema { - // Example of a custom GraphQL Middleware that sends execution result to Microsoft.Extensions.Logging API - public class GraphQLHttpMiddlewareWithLogs : GraphQLHttpMiddleware - where TSchema : ISchema - { - private readonly ILogger _logger; + private readonly ILogger _logger; - public GraphQLHttpMiddlewareWithLogs( - ILogger> logger, - IGraphQLTextSerializer requestDeserializer) - : base(requestDeserializer) - { - _logger = logger; - } + public GraphQLHttpMiddlewareWithLogs( + ILogger> logger, + IGraphQLTextSerializer requestDeserializer) + : base(requestDeserializer) + { + _logger = logger; + } - protected override Task RequestExecutedAsync(in GraphQLRequestExecutionResult requestExecutionResult) + protected override Task RequestExecutedAsync(in GraphQLRequestExecutionResult requestExecutionResult) + { + if (requestExecutionResult.Result.Errors != null) { - if (requestExecutionResult.Result.Errors != null) - { - if (requestExecutionResult.IndexInBatch.HasValue) - _logger.LogError("GraphQL execution completed in {Elapsed} with error(s) in batch [{Index}]: {Errors}", requestExecutionResult.Elapsed, requestExecutionResult.IndexInBatch, requestExecutionResult.Result.Errors); - else - _logger.LogError("GraphQL execution completed in {Elapsed} with error(s): {Errors}", requestExecutionResult.Elapsed, requestExecutionResult.Result.Errors); - } + if (requestExecutionResult.IndexInBatch.HasValue) + _logger.LogError("GraphQL execution completed in {Elapsed} with error(s) in batch [{Index}]: {Errors}", requestExecutionResult.Elapsed, requestExecutionResult.IndexInBatch, requestExecutionResult.Result.Errors); else - _logger.LogInformation("GraphQL execution successfully completed in {Elapsed}", requestExecutionResult.Elapsed); - - return base.RequestExecutedAsync(requestExecutionResult); + _logger.LogError("GraphQL execution completed in {Elapsed} with error(s): {Errors}", requestExecutionResult.Elapsed, requestExecutionResult.Result.Errors); } + else + _logger.LogInformation("GraphQL execution successfully completed in {Elapsed}", requestExecutionResult.Elapsed); - protected override CancellationToken GetCancellationToken(HttpContext context) - { - // custom CancellationToken example - var cts = CancellationTokenSource.CreateLinkedTokenSource(base.GetCancellationToken(context), new CancellationTokenSource(TimeSpan.FromSeconds(5)).Token); - return cts.Token; - } + return base.RequestExecutedAsync(requestExecutionResult); + } + + protected override CancellationToken GetCancellationToken(HttpContext context) + { + // custom CancellationToken example + var cts = CancellationTokenSource.CreateLinkedTokenSource(base.GetCancellationToken(context), new CancellationTokenSource(TimeSpan.FromSeconds(5)).Token); + return cts.Token; } } diff --git a/samples/Samples.Server/MinimumAgeRequirement.cs b/samples/Samples.Server/MinimumAgeRequirement.cs index 41f00191..8fd97b3b 100644 --- a/samples/Samples.Server/MinimumAgeRequirement.cs +++ b/samples/Samples.Server/MinimumAgeRequirement.cs @@ -1,18 +1,17 @@ using Microsoft.AspNetCore.Authorization; -namespace GraphQL.Samples.Server +namespace GraphQL.Samples.Server; + +/// +/// A enforcing a minimum user age. +/// (sample taken from https://docs.microsoft.com/en-us/aspnet/core/security/authorization/policies) +/// +public class MinimumAgeRequirement : IAuthorizationRequirement { - /// - /// A enforcing a minimum user age. - /// (sample taken from https://docs.microsoft.com/en-us/aspnet/core/security/authorization/policies) - /// - public class MinimumAgeRequirement : IAuthorizationRequirement - { - public int MinimumAge { get; } + public int MinimumAge { get; } - public MinimumAgeRequirement(int minimumAge) - { - MinimumAge = minimumAge; - } + public MinimumAgeRequirement(int minimumAge) + { + MinimumAge = minimumAge; } } diff --git a/samples/Samples.Server/Program.cs b/samples/Samples.Server/Program.cs index a8c06d26..59669cfd 100644 --- a/samples/Samples.Server/Program.cs +++ b/samples/Samples.Server/Program.cs @@ -1,39 +1,38 @@ using Serilog; using Serilog.Events; -namespace GraphQL.Samples.Server +namespace GraphQL.Samples.Server; + +public class Program { - public class Program + public static int Main(string[] args) { - public static int Main(string[] args) - { - Log.Logger = new LoggerConfiguration() - .MinimumLevel.Debug() - .MinimumLevel.Override("Microsoft", LogEventLevel.Information) - .Enrich.FromLogContext() - .WriteTo.Console() - .CreateLogger(); + Log.Logger = new LoggerConfiguration() + .MinimumLevel.Debug() + .MinimumLevel.Override("Microsoft", LogEventLevel.Information) + .Enrich.FromLogContext() + .WriteTo.Console() + .CreateLogger(); - try - { - Log.Information("Starting host"); - CreateHostBuilder(args).Build().Run(); - return 0; - } - catch (Exception ex) - { - Log.Fatal(ex, "Host terminated unexpectedly"); - return 1; - } - finally - { - Log.CloseAndFlush(); - } + try + { + Log.Information("Starting host"); + CreateHostBuilder(args).Build().Run(); + return 0; + } + catch (Exception ex) + { + Log.Fatal(ex, "Host terminated unexpectedly"); + return 1; + } + finally + { + Log.CloseAndFlush(); } - - public static IHostBuilder CreateHostBuilder(string[] args) => Host - .CreateDefaultBuilder(args) - .ConfigureWebHostDefaults(webBuilder => webBuilder.UseStartup()) - .UseSerilog(); } + + public static IHostBuilder CreateHostBuilder(string[] args) => Host + .CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => webBuilder.UseStartup()) + .UseSerilog(); } diff --git a/samples/Samples.Server/Startup.cs b/samples/Samples.Server/Startup.cs index 4f4da614..ad78f8d8 100644 --- a/samples/Samples.Server/Startup.cs +++ b/samples/Samples.Server/Startup.cs @@ -11,114 +11,113 @@ using GraphQL.Server.Ui.Voyager; using GraphQL.SystemTextJson; -namespace GraphQL.Samples.Server +namespace GraphQL.Samples.Server; + +public class Startup { - public class Startup + public Startup(IConfiguration configuration, IWebHostEnvironment environment) { - public Startup(IConfiguration configuration, IWebHostEnvironment environment) - { - Configuration = configuration; - Environment = environment; - } + Configuration = configuration; + Environment = environment; + } - public IConfiguration Configuration { get; } + public IConfiguration Configuration { get; } - public IWebHostEnvironment Environment { get; } + public IWebHostEnvironment Environment { get; } - // This method gets called by the runtime. Use this method to add services to the container. - public void ConfigureServices(IServiceCollection services) - { - services - .AddSingleton() - .Configure(opt => opt.ExposeExceptionStackTrace = Environment.IsDevelopment()) - .AddTransient(); // required by CustomErrorInfoProvider - - services.AddGraphQL(builder => builder - .AddMetrics() - .AddDocumentExecuter() - .AddHttpMiddleware>() - .AddWebSocketsHttpMiddleware() - .AddSchema() - .ConfigureExecutionOptions(options => + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services + .AddSingleton() + .Configure(opt => opt.ExposeExceptionStackTrace = Environment.IsDevelopment()) + .AddTransient(); // required by CustomErrorInfoProvider + + services.AddGraphQL(builder => builder + .AddMetrics() + .AddDocumentExecuter() + .AddHttpMiddleware>() + .AddWebSocketsHttpMiddleware() + .AddSchema() + .ConfigureExecutionOptions(options => + { + options.EnableMetrics = Environment.IsDevelopment(); + var logger = options.RequestServices.GetRequiredService>(); + options.UnhandledExceptionDelegate = ctx => { - options.EnableMetrics = Environment.IsDevelopment(); - var logger = options.RequestServices.GetRequiredService>(); - options.UnhandledExceptionDelegate = ctx => - { - logger.LogError("{Error} occurred", ctx.OriginalException.Message); - return Task.CompletedTask; - }; - }) - .AddSystemTextJson() - .AddErrorInfoProvider() - .AddWebSockets() - .AddDataLoader() - .AddGraphTypes(typeof(ChatSchema).Assembly)); - } - - // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app) - { - if (Environment.IsDevelopment()) - app.UseDeveloperExceptionPage(); + logger.LogError("{Error} occurred", ctx.OriginalException.Message); + return Task.CompletedTask; + }; + }) + .AddSystemTextJson() + .AddErrorInfoProvider() + .AddWebSockets() + .AddDataLoader() + .AddGraphTypes(typeof(ChatSchema).Assembly)); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app) + { + if (Environment.IsDevelopment()) + app.UseDeveloperExceptionPage(); - app.UseWebSockets(); + app.UseWebSockets(); - app.UseGraphQLWebSockets(); - app.UseGraphQL>(); + app.UseGraphQLWebSockets(); + app.UseGraphQL>(); - app.UseGraphQLPlayground(new PlaygroundOptions + app.UseGraphQLPlayground(new PlaygroundOptions + { + BetaUpdates = true, + RequestCredentials = RequestCredentials.Omit, + HideTracingResponse = false, + + EditorCursorShape = EditorCursorShape.Line, + EditorTheme = EditorTheme.Light, + EditorFontSize = 14, + EditorReuseHeaders = true, + EditorFontFamily = "Consolas", + + PrettierPrintWidth = 80, + PrettierTabWidth = 2, + PrettierUseTabs = true, + + SchemaDisableComments = false, + SchemaPollingEnabled = true, + SchemaPollingEndpointFilter = "*localhost*", + SchemaPollingInterval = 5000, + + Headers = new Dictionary { - BetaUpdates = true, - RequestCredentials = RequestCredentials.Omit, - HideTracingResponse = false, - - EditorCursorShape = EditorCursorShape.Line, - EditorTheme = EditorTheme.Light, - EditorFontSize = 14, - EditorReuseHeaders = true, - EditorFontFamily = "Consolas", - - PrettierPrintWidth = 80, - PrettierTabWidth = 2, - PrettierUseTabs = true, - - SchemaDisableComments = false, - SchemaPollingEnabled = true, - SchemaPollingEndpointFilter = "*localhost*", - SchemaPollingInterval = 5000, - - Headers = new Dictionary - { - ["MyHeader1"] = "MyValue", - ["MyHeader2"] = 42, - }, - }); + ["MyHeader1"] = "MyValue", + ["MyHeader2"] = 42, + }, + }); - app.UseGraphQLGraphiQL(new GraphiQLOptions + app.UseGraphQLGraphiQL(new GraphiQLOptions + { + Headers = new Dictionary { - Headers = new Dictionary - { - ["X-api-token"] = "130fh9823bd023hd892d0j238dh", - } - }); + ["X-api-token"] = "130fh9823bd023hd892d0j238dh", + } + }); - app.UseGraphQLAltair(new AltairOptions + app.UseGraphQLAltair(new AltairOptions + { + Headers = new Dictionary { - Headers = new Dictionary - { - ["X-api-token"] = "130fh9823bd023hd892d0j238dh", - } - }); + ["X-api-token"] = "130fh9823bd023hd892d0j238dh", + } + }); - app.UseGraphQLVoyager(new VoyagerOptions + app.UseGraphQLVoyager(new VoyagerOptions + { + Headers = new Dictionary { - Headers = new Dictionary - { - ["MyHeader1"] = "MyValue", - ["MyHeader2"] = 42, - }, - }); - } + ["MyHeader1"] = "MyValue", + ["MyHeader2"] = 42, + }, + }); } } diff --git a/samples/Samples.Server/StartupWithRouting.cs b/samples/Samples.Server/StartupWithRouting.cs index d50ef957..b2824f04 100644 --- a/samples/Samples.Server/StartupWithRouting.cs +++ b/samples/Samples.Server/StartupWithRouting.cs @@ -11,121 +11,120 @@ using GraphQL.Server.Ui.Voyager; using GraphQL.SystemTextJson; -namespace GraphQL.Samples.Server +namespace GraphQL.Samples.Server; + +public class StartupWithRouting { - public class StartupWithRouting + public StartupWithRouting(IConfiguration configuration, IWebHostEnvironment environment) { - public StartupWithRouting(IConfiguration configuration, IWebHostEnvironment environment) - { - Configuration = configuration; - Environment = environment; - } + Configuration = configuration; + Environment = environment; + } - public IConfiguration Configuration { get; } + public IConfiguration Configuration { get; } - public IWebHostEnvironment Environment { get; } + public IWebHostEnvironment Environment { get; } - // This method gets called by the runtime. Use this method to add services to the container. - public void ConfigureServices(IServiceCollection services) - { - services - .AddRouting() - .AddSingleton() - .Configure(opt => opt.ExposeExceptionStackTrace = Environment.IsDevelopment()) - .AddTransient(); // required by CustomErrorInfoProvider - - services.AddGraphQL(builder => builder - .AddMetrics() - .AddDocumentExecuter() - .AddHttpMiddleware>() - .AddWebSocketsHttpMiddleware() - .AddSchema() - .ConfigureExecutionOptions(options => + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services + .AddRouting() + .AddSingleton() + .Configure(opt => opt.ExposeExceptionStackTrace = Environment.IsDevelopment()) + .AddTransient(); // required by CustomErrorInfoProvider + + services.AddGraphQL(builder => builder + .AddMetrics() + .AddDocumentExecuter() + .AddHttpMiddleware>() + .AddWebSocketsHttpMiddleware() + .AddSchema() + .ConfigureExecutionOptions(options => + { + options.EnableMetrics = Environment.IsDevelopment(); + var logger = options.RequestServices.GetRequiredService>(); + options.UnhandledExceptionDelegate = ctx => { - options.EnableMetrics = Environment.IsDevelopment(); - var logger = options.RequestServices.GetRequiredService>(); - options.UnhandledExceptionDelegate = ctx => - { - logger.LogError("{Error} occurred", ctx.OriginalException.Message); - return Task.CompletedTask; - }; - }) - .AddDefaultEndpointSelectorPolicy() - .AddSystemTextJson() - .AddErrorInfoProvider() - .AddWebSockets() - .AddDataLoader() - .AddGraphTypes(typeof(ChatSchema).Assembly)); - } - - // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app) - { - if (Environment.IsDevelopment()) - app.UseDeveloperExceptionPage(); + logger.LogError("{Error} occurred", ctx.OriginalException.Message); + return Task.CompletedTask; + }; + }) + .AddDefaultEndpointSelectorPolicy() + .AddSystemTextJson() + .AddErrorInfoProvider() + .AddWebSockets() + .AddDataLoader() + .AddGraphTypes(typeof(ChatSchema).Assembly)); + } - app.UseWebSockets(); + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app) + { + if (Environment.IsDevelopment()) + app.UseDeveloperExceptionPage(); - app.UseRouting(); + app.UseWebSockets(); - app.UseEndpoints(endpoints => - { - endpoints.MapGraphQLWebSockets(); - endpoints.MapGraphQL>(); + app.UseRouting(); - endpoints.MapGraphQLPlayground(new PlaygroundOptions + app.UseEndpoints(endpoints => + { + endpoints.MapGraphQLWebSockets(); + endpoints.MapGraphQL>(); + + endpoints.MapGraphQLPlayground(new PlaygroundOptions + { + BetaUpdates = true, + RequestCredentials = RequestCredentials.Omit, + HideTracingResponse = false, + + EditorCursorShape = EditorCursorShape.Line, + EditorTheme = EditorTheme.Light, + EditorFontSize = 14, + EditorReuseHeaders = true, + EditorFontFamily = "Consolas", + + PrettierPrintWidth = 80, + PrettierTabWidth = 2, + PrettierUseTabs = true, + + SchemaDisableComments = false, + SchemaPollingEnabled = true, + SchemaPollingEndpointFilter = "*localhost*", + SchemaPollingInterval = 5000, + + Headers = new Dictionary { - BetaUpdates = true, - RequestCredentials = RequestCredentials.Omit, - HideTracingResponse = false, - - EditorCursorShape = EditorCursorShape.Line, - EditorTheme = EditorTheme.Light, - EditorFontSize = 14, - EditorReuseHeaders = true, - EditorFontFamily = "Consolas", - - PrettierPrintWidth = 80, - PrettierTabWidth = 2, - PrettierUseTabs = true, - - SchemaDisableComments = false, - SchemaPollingEnabled = true, - SchemaPollingEndpointFilter = "*localhost*", - SchemaPollingInterval = 5000, - - Headers = new Dictionary - { - ["MyHeader1"] = "MyValue", - ["MyHeader2"] = 42, - }, - }); - - endpoints.MapGraphQLGraphiQL(new GraphiQLOptions + ["MyHeader1"] = "MyValue", + ["MyHeader2"] = 42, + }, + }); + + endpoints.MapGraphQLGraphiQL(new GraphiQLOptions + { + Headers = new Dictionary { - Headers = new Dictionary - { - ["X-api-token"] = "130fh9823bd023hd892d0j238dh", - } - }); + ["X-api-token"] = "130fh9823bd023hd892d0j238dh", + } + }); - endpoints.MapGraphQLAltair(new AltairOptions + endpoints.MapGraphQLAltair(new AltairOptions + { + Headers = new Dictionary { - Headers = new Dictionary - { - ["X-api-token"] = "130fh9823bd023hd892d0j238dh", - } - }); + ["X-api-token"] = "130fh9823bd023hd892d0j238dh", + } + }); - endpoints.MapGraphQLVoyager(new VoyagerOptions + endpoints.MapGraphQLVoyager(new VoyagerOptions + { + Headers = new Dictionary { - Headers = new Dictionary - { - ["MyHeader1"] = "MyValue", - ["MyHeader2"] = 42, - }, - }); + ["MyHeader1"] = "MyValue", + ["MyHeader2"] = 42, + }, }); - } + }); } } diff --git a/src/Authorization.AspNetCore/AuthorizationError.cs b/src/Authorization.AspNetCore/AuthorizationError.cs index 25f17bb2..a02f2126 100644 --- a/src/Authorization.AspNetCore/AuthorizationError.cs +++ b/src/Authorization.AspNetCore/AuthorizationError.cs @@ -4,32 +4,31 @@ using GraphQLParser.AST; using Microsoft.AspNetCore.Authorization; -namespace GraphQL.Server.Authorization.AspNetCore +namespace GraphQL.Server.Authorization.AspNetCore; + +/// +/// An error that represents an authorization failure while parsing the document. +/// +public class AuthorizationError : ValidationError { /// - /// An error that represents an authorization failure while parsing the document. + /// Initializes a new instance of the class for a specified authorization result with a specific error message. /// - public class AuthorizationError : ValidationError + public AuthorizationError(ASTNode? node, ValidationContext context, string message, AuthorizationResult result, OperationType? operationType = null) + : base(context.Document.Source, "6.1.1", message, node == null ? Array.Empty() : new ASTNode[] { node }) { - /// - /// Initializes a new instance of the class for a specified authorization result with a specific error message. - /// - public AuthorizationError(ASTNode? node, ValidationContext context, string message, AuthorizationResult result, OperationType? operationType = null) - : base(context.Document.Source, "6.1.1", message, node == null ? Array.Empty() : new ASTNode[] { node }) - { - Code = "authorization"; - AuthorizationResult = result; - OperationType = operationType; - } + Code = "authorization"; + AuthorizationResult = result; + OperationType = operationType; + } - /// - /// Returns the result of the ASP.NET Core authorization request. - /// - public virtual AuthorizationResult AuthorizationResult { get; } + /// + /// Returns the result of the ASP.NET Core authorization request. + /// + public virtual AuthorizationResult AuthorizationResult { get; } - /// - /// The GraphQL operation type. - /// - public OperationType? OperationType { get; } - } + /// + /// The GraphQL operation type. + /// + public OperationType? OperationType { get; } } diff --git a/src/Authorization.AspNetCore/AuthorizationValidationRule.cs b/src/Authorization.AspNetCore/AuthorizationValidationRule.cs index 1d57ae08..4934d7cb 100644 --- a/src/Authorization.AspNetCore/AuthorizationValidationRule.cs +++ b/src/Authorization.AspNetCore/AuthorizationValidationRule.cs @@ -8,208 +8,207 @@ using GraphQLParser.Visitors; using Microsoft.AspNetCore.Authorization; -namespace GraphQL.Server.Authorization.AspNetCore +namespace GraphQL.Server.Authorization.AspNetCore; + +/// +/// GraphQL authorization validation rule which integrates to ASP.NET Core authorization mechanism. +/// For more information see https://docs.microsoft.com/en-us/aspnet/core/security/authorization/introduction. +/// +public class AuthorizationValidationRule : IValidationRule { + private readonly IAuthorizationService _authorizationService; + private readonly IClaimsPrincipalAccessor _claimsPrincipalAccessor; + private readonly IAuthorizationErrorMessageBuilder _messageBuilder; + /// - /// GraphQL authorization validation rule which integrates to ASP.NET Core authorization mechanism. - /// For more information see https://docs.microsoft.com/en-us/aspnet/core/security/authorization/introduction. + /// Creates an instance of . /// - public class AuthorizationValidationRule : IValidationRule + /// ASP.NET Core to authorize against. + /// The which provides the for authorization. + /// The which is used to generate the message for an . + public AuthorizationValidationRule( + IAuthorizationService authorizationService, + IClaimsPrincipalAccessor claimsPrincipalAccessor, + IAuthorizationErrorMessageBuilder messageBuilder) + { + _authorizationService = authorizationService; + _claimsPrincipalAccessor = claimsPrincipalAccessor; + _messageBuilder = messageBuilder; + } + + private bool ShouldBeSkipped(GraphQLOperationDefinition actualOperation, ValidationContext context) { - private readonly IAuthorizationService _authorizationService; - private readonly IClaimsPrincipalAccessor _claimsPrincipalAccessor; - private readonly IAuthorizationErrorMessageBuilder _messageBuilder; - - /// - /// Creates an instance of . - /// - /// ASP.NET Core to authorize against. - /// The which provides the for authorization. - /// The which is used to generate the message for an . - public AuthorizationValidationRule( - IAuthorizationService authorizationService, - IClaimsPrincipalAccessor claimsPrincipalAccessor, - IAuthorizationErrorMessageBuilder messageBuilder) + if (context.Document.OperationsCount() <= 1) { - _authorizationService = authorizationService; - _claimsPrincipalAccessor = claimsPrincipalAccessor; - _messageBuilder = messageBuilder; + return false; } - private bool ShouldBeSkipped(GraphQLOperationDefinition actualOperation, ValidationContext context) + int i = 0; + do { - if (context.Document.OperationsCount() <= 1) + var ancestor = context.TypeInfo.GetAncestor(i++); + + if (ancestor == actualOperation) { return false; } - int i = 0; - do + if (ancestor == context.Document) { - var ancestor = context.TypeInfo.GetAncestor(i++); - - if (ancestor == actualOperation) - { - return false; - } - - if (ancestor == context.Document) - { - return true; - } - - if (ancestor is GraphQLFragmentDefinition fragment) - { - //TODO: may be rewritten completely later - var c = new FragmentBelongsToOperationVisitorContext(fragment); - _visitor.VisitAsync(actualOperation, c).GetAwaiter().GetResult(); // TODO: need to think of something to avoid this - return !c.Found; - } - } while (true); - } + return true; + } - private sealed class FragmentBelongsToOperationVisitorContext : IASTVisitorContext - { - public FragmentBelongsToOperationVisitorContext(GraphQLFragmentDefinition fragment) + if (ancestor is GraphQLFragmentDefinition fragment) { - Fragment = fragment; + //TODO: may be rewritten completely later + var c = new FragmentBelongsToOperationVisitorContext(fragment); + _visitor.VisitAsync(actualOperation, c).GetAwaiter().GetResult(); // TODO: need to think of something to avoid this + return !c.Found; } + } while (true); + } - public GraphQLFragmentDefinition Fragment { get; } + private sealed class FragmentBelongsToOperationVisitorContext : IASTVisitorContext + { + public FragmentBelongsToOperationVisitorContext(GraphQLFragmentDefinition fragment) + { + Fragment = fragment; + } - public bool Found { get; set; } + public GraphQLFragmentDefinition Fragment { get; } - public CancellationToken CancellationToken => default; - } + public bool Found { get; set; } - private static readonly FragmentBelongsToOperationVisitor _visitor = new(); + public CancellationToken CancellationToken => default; + } - private sealed class FragmentBelongsToOperationVisitor : ASTVisitor - { - protected override ValueTask VisitFragmentSpreadAsync(GraphQLFragmentSpread fragmentSpread, FragmentBelongsToOperationVisitorContext context) - { - context.Found = context.Fragment.FragmentName.Name == fragmentSpread.FragmentName.Name; - return default; - } + private static readonly FragmentBelongsToOperationVisitor _visitor = new(); - public override ValueTask VisitAsync(ASTNode? node, FragmentBelongsToOperationVisitorContext context) - { - return context.Found ? default : base.VisitAsync(node, context); - } + private sealed class FragmentBelongsToOperationVisitor : ASTVisitor + { + protected override ValueTask VisitFragmentSpreadAsync(GraphQLFragmentSpread fragmentSpread, FragmentBelongsToOperationVisitorContext context) + { + context.Found = context.Fragment.FragmentName.Name == fragmentSpread.FragmentName.Name; + return default; } - /// - public async ValueTask ValidateAsync(ValidationContext context) + public override ValueTask VisitAsync(ASTNode? node, FragmentBelongsToOperationVisitorContext context) { - await AuthorizeAsync(null, context.Schema, context, null); - var operationType = OperationType.Query; - - // this could leak info about hidden fields or types in error messages - // it would be better to implement a filter on the Schema so it - // acts as if they just don't exist vs. an auth denied error - // - filtering the Schema is not currently supported - // TODO: apply ISchemaFilter - context.Schema.Filter.AllowXXX - return new NodeVisitors( - new MatchingNodeVisitor((astType, context) => + return context.Found ? default : base.VisitAsync(node, context); + } + } + + /// + public async ValueTask ValidateAsync(ValidationContext context) + { + await AuthorizeAsync(null, context.Schema, context, null); + var operationType = OperationType.Query; + + // this could leak info about hidden fields or types in error messages + // it would be better to implement a filter on the Schema so it + // acts as if they just don't exist vs. an auth denied error + // - filtering the Schema is not currently supported + // TODO: apply ISchemaFilter - context.Schema.Filter.AllowXXX + return new NodeVisitors( + new MatchingNodeVisitor((astType, context) => + { + if (context.Document.OperationsCount() > 1 && astType.Name != context.Operation.Name) { - if (context.Document.OperationsCount() > 1 && astType.Name != context.Operation.Name) - { - return; - } + return; + } - operationType = astType.Operation; + operationType = astType.Operation; - var type = context.TypeInfo.GetLastType(); - AuthorizeAsync(astType, type, context, operationType).GetAwaiter().GetResult(); // TODO: need to think of something to avoid this - }), + var type = context.TypeInfo.GetLastType(); + AuthorizeAsync(astType, type, context, operationType).GetAwaiter().GetResult(); // TODO: need to think of something to avoid this + }), - new MatchingNodeVisitor((objectFieldAst, context) => + new MatchingNodeVisitor((objectFieldAst, context) => + { + if (context.TypeInfo.GetArgument()?.ResolvedType?.GetNamedType() is IComplexGraphType argumentType && !ShouldBeSkipped(context.Operation, context)) { - if (context.TypeInfo.GetArgument()?.ResolvedType?.GetNamedType() is IComplexGraphType argumentType && !ShouldBeSkipped(context.Operation, context)) - { - var fieldType = argumentType.GetField(objectFieldAst.Name); - AuthorizeAsync(objectFieldAst, fieldType, context, operationType).GetAwaiter().GetResult(); // TODO: need to think of something to avoid this - } - }), + var fieldType = argumentType.GetField(objectFieldAst.Name); + AuthorizeAsync(objectFieldAst, fieldType, context, operationType).GetAwaiter().GetResult(); // TODO: need to think of something to avoid this + } + }), - new MatchingNodeVisitor((fieldAst, context) => - { - var fieldDef = context.TypeInfo.GetFieldDef(); + new MatchingNodeVisitor((fieldAst, context) => + { + var fieldDef = context.TypeInfo.GetFieldDef(); - if (fieldDef == null || ShouldBeSkipped(context.Operation, context)) - return; + if (fieldDef == null || ShouldBeSkipped(context.Operation, context)) + return; - // check target field - AuthorizeAsync(fieldAst, fieldDef, context, operationType).GetAwaiter().GetResult(); // TODO: need to think of something to avoid this - // check returned graph type - AuthorizeAsync(fieldAst, fieldDef.ResolvedType?.GetNamedType(), context, operationType).GetAwaiter().GetResult(); // TODO: need to think of something to avoid this - }), + // check target field + AuthorizeAsync(fieldAst, fieldDef, context, operationType).GetAwaiter().GetResult(); // TODO: need to think of something to avoid this + // check returned graph type + AuthorizeAsync(fieldAst, fieldDef.ResolvedType?.GetNamedType(), context, operationType).GetAwaiter().GetResult(); // TODO: need to think of something to avoid this + }), - new MatchingNodeVisitor((variableRef, context) => + new MatchingNodeVisitor((variableRef, context) => + { + if (context.TypeInfo.GetArgument()?.ResolvedType?.GetNamedType() is not IComplexGraphType variableType || ShouldBeSkipped(context.Operation, context)) + return; + + AuthorizeAsync(variableRef, variableType, context, operationType).GetAwaiter().GetResult(); // TODO: need to think of something to avoid this; + + // Check each supplied field in the variable that exists in the variable type. + // If some supplied field does not exist in the variable type then some other + // validation rule should check that but here we should just ignore that + // "unknown" field. + if (context.Variables != null && + context.Variables.TryGetValue(variableRef.Name.StringValue, out object? input) && //ISSUE:allocation + input is Dictionary fieldsValues) { - if (context.TypeInfo.GetArgument()?.ResolvedType?.GetNamedType() is not IComplexGraphType variableType || ShouldBeSkipped(context.Operation, context)) - return; - - AuthorizeAsync(variableRef, variableType, context, operationType).GetAwaiter().GetResult(); // TODO: need to think of something to avoid this; - - // Check each supplied field in the variable that exists in the variable type. - // If some supplied field does not exist in the variable type then some other - // validation rule should check that but here we should just ignore that - // "unknown" field. - if (context.Variables != null && - context.Variables.TryGetValue(variableRef.Name.StringValue, out object? input) && //ISSUE:allocation - input is Dictionary fieldsValues) + foreach (var field in variableType.Fields) { - foreach (var field in variableType.Fields) + if (fieldsValues.ContainsKey(field.Name)) { - if (fieldsValues.ContainsKey(field.Name)) - { - AuthorizeAsync(variableRef, field, context, operationType).GetAwaiter().GetResult(); // TODO: need to think of something to avoid this; - } + AuthorizeAsync(variableRef, field, context, operationType).GetAwaiter().GetResult(); // TODO: need to think of something to avoid this; } } - }) - ); - } + } + }) + ); + } - private async Task AuthorizeAsync(ASTNode? node, IProvideMetadata? provider, ValidationContext context, OperationType? operationType) - { - var policyNames = provider?.GetPolicies(); + private async Task AuthorizeAsync(ASTNode? node, IProvideMetadata? provider, ValidationContext context, OperationType? operationType) + { + var policyNames = provider?.GetPolicies(); - if (policyNames?.Count == 1) + if (policyNames?.Count == 1) + { + // small optimization for the single policy - no 'new List<>()', no 'await Task.WhenAll()' + var authorizationResult = await _authorizationService.AuthorizeAsync(_claimsPrincipalAccessor.GetClaimsPrincipal(context), policyNames[0]); + if (!authorizationResult.Succeeded) + AddValidationError(node, context, operationType, authorizationResult); + } + else if (policyNames?.Count > 1) + { + var claimsPrincipal = _claimsPrincipalAccessor.GetClaimsPrincipal(context); + var tasks = new List>(policyNames.Count); + foreach (string policyName in policyNames) { - // small optimization for the single policy - no 'new List<>()', no 'await Task.WhenAll()' - var authorizationResult = await _authorizationService.AuthorizeAsync(_claimsPrincipalAccessor.GetClaimsPrincipal(context), policyNames[0]); - if (!authorizationResult.Succeeded) - AddValidationError(node, context, operationType, authorizationResult); + var task = _authorizationService.AuthorizeAsync(claimsPrincipal, policyName); + tasks.Add(task); } - else if (policyNames?.Count > 1) - { - var claimsPrincipal = _claimsPrincipalAccessor.GetClaimsPrincipal(context); - var tasks = new List>(policyNames.Count); - foreach (string policyName in policyNames) - { - var task = _authorizationService.AuthorizeAsync(claimsPrincipal, policyName); - tasks.Add(task); - } - var authorizationResults = await Task.WhenAll(tasks); + var authorizationResults = await Task.WhenAll(tasks); - foreach (var result in authorizationResults) - { - if (!result.Succeeded) - AddValidationError(node, context, operationType, result); - } + foreach (var result in authorizationResults) + { + if (!result.Succeeded) + AddValidationError(node, context, operationType, result); } } + } - /// - /// Adds an authorization failure error to the document response - /// - protected virtual void AddValidationError(ASTNode? node, ValidationContext context, OperationType? operationType, AuthorizationResult result) - { - string message = _messageBuilder.GenerateMessage(operationType, result); - context.ReportError(new AuthorizationError(node, context, message, result, operationType)); - } + /// + /// Adds an authorization failure error to the document response + /// + protected virtual void AddValidationError(ASTNode? node, ValidationContext context, OperationType? operationType, AuthorizationResult result) + { + string message = _messageBuilder.GenerateMessage(operationType, result); + context.ReportError(new AuthorizationError(node, context, message, result, operationType)); } } diff --git a/src/Authorization.AspNetCore/DefaultAuthorizationErrorMessageBuilder.cs b/src/Authorization.AspNetCore/DefaultAuthorizationErrorMessageBuilder.cs index b82de8f6..14905947 100644 --- a/src/Authorization.AspNetCore/DefaultAuthorizationErrorMessageBuilder.cs +++ b/src/Authorization.AspNetCore/DefaultAuthorizationErrorMessageBuilder.cs @@ -5,109 +5,108 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization.Infrastructure; -namespace GraphQL.Server.Authorization.AspNetCore +namespace GraphQL.Server.Authorization.AspNetCore; + +public class DefaultAuthorizationErrorMessageBuilder : IAuthorizationErrorMessageBuilder { - public class DefaultAuthorizationErrorMessageBuilder : IAuthorizationErrorMessageBuilder + /// + public virtual string GenerateMessage(OperationType? operationType, AuthorizationResult result) { - /// - public virtual string GenerateMessage(OperationType? operationType, AuthorizationResult result) - { - if (result.Succeeded) - return "Success!"; + if (result.Succeeded) + return "Success!"; - var error = new StringBuilder(); - AppendFailureHeader(error, operationType); + var error = new StringBuilder(); + AppendFailureHeader(error, operationType); - if (result.Failure != null) + if (result.Failure != null) + { + foreach (var requirement in result.Failure.FailedRequirements) { - foreach (var requirement in result.Failure.FailedRequirements) - { - AppendFailureLine(error, requirement); - } + AppendFailureLine(error, requirement); } - - return error.ToString(); } - private string GetOperationType(OperationType? operationType) - { - return operationType switch - { - OperationType.Query => "query", - OperationType.Mutation => "mutation", - OperationType.Subscription => "subscription", - _ => "operation", - }; - } + return error.ToString(); + } - /// - public virtual void AppendFailureHeader(StringBuilder error, OperationType? operationType) + private string GetOperationType(OperationType? operationType) + { + return operationType switch { - error - .Append("You are not authorized to run this ") - .Append(GetOperationType(operationType)) - .Append('.'); - } + OperationType.Query => "query", + OperationType.Mutation => "mutation", + OperationType.Subscription => "subscription", + _ => "operation", + }; + } - /// - public virtual void AppendFailureLine(StringBuilder error, IAuthorizationRequirement authorizationRequirement) - { - error.AppendLine(); + /// + public virtual void AppendFailureHeader(StringBuilder error, OperationType? operationType) + { + error + .Append("You are not authorized to run this ") + .Append(GetOperationType(operationType)) + .Append('.'); + } - switch (authorizationRequirement) - { - case ClaimsAuthorizationRequirement claimsAuthorizationRequirement: - error.Append("Required claim '"); - error.Append(claimsAuthorizationRequirement.ClaimType); - if (claimsAuthorizationRequirement.AllowedValues == null || !claimsAuthorizationRequirement.AllowedValues.Any()) - { - error.Append("' is not present."); - } - else - { - error.Append("' with any value of '"); - error.Append(string.Join(", ", claimsAuthorizationRequirement.AllowedValues)); - error.Append("' is not present."); - } - break; + /// + public virtual void AppendFailureLine(StringBuilder error, IAuthorizationRequirement authorizationRequirement) + { + error.AppendLine(); + + switch (authorizationRequirement) + { + case ClaimsAuthorizationRequirement claimsAuthorizationRequirement: + error.Append("Required claim '"); + error.Append(claimsAuthorizationRequirement.ClaimType); + if (claimsAuthorizationRequirement.AllowedValues == null || !claimsAuthorizationRequirement.AllowedValues.Any()) + { + error.Append("' is not present."); + } + else + { + error.Append("' with any value of '"); + error.Append(string.Join(", ", claimsAuthorizationRequirement.AllowedValues)); + error.Append("' is not present."); + } + break; - case DenyAnonymousAuthorizationRequirement _: - error.Append("The current user must be authenticated."); - break; + case DenyAnonymousAuthorizationRequirement _: + error.Append("The current user must be authenticated."); + break; - case NameAuthorizationRequirement nameAuthorizationRequirement: - error.Append("The current user name must match the name '"); - error.Append(nameAuthorizationRequirement.RequiredName); - error.Append("'."); - break; + case NameAuthorizationRequirement nameAuthorizationRequirement: + error.Append("The current user name must match the name '"); + error.Append(nameAuthorizationRequirement.RequiredName); + error.Append("'."); + break; - case OperationAuthorizationRequirement operationAuthorizationRequirement: - error.Append("Required operation '"); - error.Append(operationAuthorizationRequirement.Name); - error.Append("' was not present."); - break; + case OperationAuthorizationRequirement operationAuthorizationRequirement: + error.Append("Required operation '"); + error.Append(operationAuthorizationRequirement.Name); + error.Append("' was not present."); + break; - case RolesAuthorizationRequirement rolesAuthorizationRequirement: - if (rolesAuthorizationRequirement.AllowedRoles == null || !rolesAuthorizationRequirement.AllowedRoles.Any()) - { - // This should never happen. - error.Append("Required roles are not present."); - } - else - { - error.Append("Required roles '"); - error.Append(string.Join(", ", rolesAuthorizationRequirement.AllowedRoles)); - error.Append("' are not present."); - } - break; + case RolesAuthorizationRequirement rolesAuthorizationRequirement: + if (rolesAuthorizationRequirement.AllowedRoles == null || !rolesAuthorizationRequirement.AllowedRoles.Any()) + { + // This should never happen. + error.Append("Required roles are not present."); + } + else + { + error.Append("Required roles '"); + error.Append(string.Join(", ", rolesAuthorizationRequirement.AllowedRoles)); + error.Append("' are not present."); + } + break; - case AssertionRequirement _: - default: - error.Append("Requirement '"); - error.Append(authorizationRequirement.GetType().Name); - error.Append("' was not satisfied."); - break; - } + case AssertionRequirement _: + default: + error.Append("Requirement '"); + error.Append(authorizationRequirement.GetType().Name); + error.Append("' was not satisfied."); + break; } } } diff --git a/src/Authorization.AspNetCore/DefaultClaimsPrincipalAccessor.cs b/src/Authorization.AspNetCore/DefaultClaimsPrincipalAccessor.cs index 8b849711..b3204eae 100644 --- a/src/Authorization.AspNetCore/DefaultClaimsPrincipalAccessor.cs +++ b/src/Authorization.AspNetCore/DefaultClaimsPrincipalAccessor.cs @@ -2,32 +2,31 @@ using GraphQL.Validation; using Microsoft.AspNetCore.Http; -namespace GraphQL.Server.Authorization.AspNetCore +namespace GraphQL.Server.Authorization.AspNetCore; + +/// +/// The default claims principal accessor. +/// +public class DefaultClaimsPrincipalAccessor : IClaimsPrincipalAccessor { + private readonly IHttpContextAccessor _contextAccessor; + /// - /// The default claims principal accessor. + /// Creates an instance of . /// - public class DefaultClaimsPrincipalAccessor : IClaimsPrincipalAccessor + /// ASP.NET Core to take claims principal () from. + public DefaultClaimsPrincipalAccessor(IHttpContextAccessor contextAccessor) { - private readonly IHttpContextAccessor _contextAccessor; - - /// - /// Creates an instance of . - /// - /// ASP.NET Core to take claims principal () from. - public DefaultClaimsPrincipalAccessor(IHttpContextAccessor contextAccessor) - { - _contextAccessor = contextAccessor ?? throw new ArgumentNullException(nameof(contextAccessor)); - } + _contextAccessor = contextAccessor ?? throw new ArgumentNullException(nameof(contextAccessor)); + } - /// - /// Returns the . - /// - /// - /// - public ClaimsPrincipal GetClaimsPrincipal(ValidationContext context) - { - return _contextAccessor.HttpContext?.User; - } + /// + /// Returns the . + /// + /// + /// + public ClaimsPrincipal GetClaimsPrincipal(ValidationContext context) + { + return _contextAccessor.HttpContext?.User; } } diff --git a/src/Authorization.AspNetCore/GraphQLBuilderAuthorizationExtensions.cs b/src/Authorization.AspNetCore/GraphQLBuilderAuthorizationExtensions.cs index bc5a09e8..307052a8 100644 --- a/src/Authorization.AspNetCore/GraphQLBuilderAuthorizationExtensions.cs +++ b/src/Authorization.AspNetCore/GraphQLBuilderAuthorizationExtensions.cs @@ -6,41 +6,40 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; -namespace GraphQL.Server +namespace GraphQL.Server; + +public static class GraphQLBuilderAuthorizationExtensions { - public static class GraphQLBuilderAuthorizationExtensions + /// + /// Adds the GraphQL authorization. + /// + /// The GraphQL builder. + /// Reference to the passed . + public static IGraphQLBuilder AddGraphQLAuthorization(this IGraphQLBuilder builder) + => builder.AddGraphQLAuthorization(_ => { }); + + /// + /// Adds the GraphQL authorization. + /// + /// The GraphQL builder. + /// An action delegate to configure the provided . + /// Reference to the passed . + public static IGraphQLBuilder AddGraphQLAuthorization(this IGraphQLBuilder builder, Action? configure) { - /// - /// Adds the GraphQL authorization. - /// - /// The GraphQL builder. - /// Reference to the passed . - public static IGraphQLBuilder AddGraphQLAuthorization(this IGraphQLBuilder builder) - => builder.AddGraphQLAuthorization(_ => { }); - - /// - /// Adds the GraphQL authorization. - /// - /// The GraphQL builder. - /// An action delegate to configure the provided . - /// Reference to the passed . - public static IGraphQLBuilder AddGraphQLAuthorization(this IGraphQLBuilder builder, Action? configure) - { - if (builder.Services is not IServiceCollection services) - throw new NotSupportedException("This method only supports the MicrosoftDI implementation of IGraphQLBuilder."); - - services.TryAddTransient(); - services.TryAddTransient(); - services.AddHttpContextAccessor(); - - if (configure != null) - services.AddAuthorizationCore(configure); - else - services.AddAuthorizationCore(); - - builder.AddValidationRule(); - - return builder; - } + if (builder.Services is not IServiceCollection services) + throw new NotSupportedException("This method only supports the MicrosoftDI implementation of IGraphQLBuilder."); + + services.TryAddTransient(); + services.TryAddTransient(); + services.AddHttpContextAccessor(); + + if (configure != null) + services.AddAuthorizationCore(configure); + else + services.AddAuthorizationCore(); + + builder.AddValidationRule(); + + return builder; } } diff --git a/src/Authorization.AspNetCore/IClaimsPrincipalAccessor.cs b/src/Authorization.AspNetCore/IClaimsPrincipalAccessor.cs index 9f6cf57e..3d475d08 100644 --- a/src/Authorization.AspNetCore/IClaimsPrincipalAccessor.cs +++ b/src/Authorization.AspNetCore/IClaimsPrincipalAccessor.cs @@ -1,18 +1,17 @@ using System.Security.Claims; using GraphQL.Validation; -namespace GraphQL.Server.Authorization.AspNetCore +namespace GraphQL.Server.Authorization.AspNetCore; + +/// +/// Provides access to the used for GraphQL operation authorization. +/// +public interface IClaimsPrincipalAccessor { /// - /// Provides access to the used for GraphQL operation authorization. + /// Provides the for the current /// - public interface IClaimsPrincipalAccessor - { - /// - /// Provides the for the current - /// - /// The of the current operation - /// - ClaimsPrincipal GetClaimsPrincipal(ValidationContext context); - } + /// The of the current operation + /// + ClaimsPrincipal GetClaimsPrincipal(ValidationContext context); } diff --git a/src/Benchmarks/Benchmarks/DeserializeFromJsonBodyBenchmark.cs b/src/Benchmarks/Benchmarks/DeserializeFromJsonBodyBenchmark.cs index 1c492907..ce2f27f6 100644 --- a/src/Benchmarks/Benchmarks/DeserializeFromJsonBodyBenchmark.cs +++ b/src/Benchmarks/Benchmarks/DeserializeFromJsonBodyBenchmark.cs @@ -4,18 +4,18 @@ using NsjDeserializer = GraphQL.NewtonsoftJson.GraphQLSerializer; using StjDeserializer = GraphQL.SystemTextJson.GraphQLSerializer; -namespace GraphQL.Server.Benchmarks +namespace GraphQL.Server.Benchmarks; + +[MemoryDiagnoser] +[RPlotExporter, CsvMeasurementsExporter] +public class DeserializeFromJsonBodyBenchmark { - [MemoryDiagnoser] - [RPlotExporter, CsvMeasurementsExporter] - public class DeserializeFromJsonBodyBenchmark - { - private NsjDeserializer _nsjDeserializer; - private StjDeserializer _stjDeserializer; - private Stream _httpRequestBody; - private Stream _httpRequestBody2; + private NsjDeserializer _nsjDeserializer; + private StjDeserializer _stjDeserializer; + private Stream _httpRequestBody; + private Stream _httpRequestBody2; - private const string SHORT_JSON = @"{ + private const string SHORT_JSON = @"{ ""key0"": null, ""key1"": true, ""key2"": 1.2, @@ -29,45 +29,44 @@ public class DeserializeFromJsonBodyBenchmark } }"; - [GlobalSetup] - public void GlobalSetup() - { - _nsjDeserializer = new NsjDeserializer(s => { }); - _stjDeserializer = new StjDeserializer(s => { }); + [GlobalSetup] + public void GlobalSetup() + { + _nsjDeserializer = new NsjDeserializer(s => { }); + _stjDeserializer = new StjDeserializer(s => { }); - var gqlRequest = new GraphQLRequest { Query = SchemaIntrospection.IntrospectionQuery }; - var gqlRequestJson = Serializer.ToJson(gqlRequest); - _httpRequestBody = GetHttpRequestBodyFor(gqlRequestJson); + var gqlRequest = new GraphQLRequest { Query = SchemaIntrospection.IntrospectionQuery }; + var gqlRequestJson = Serializer.ToJson(gqlRequest); + _httpRequestBody = GetHttpRequestBodyFor(gqlRequestJson); - gqlRequest.OperationName = "someOperationName"; - gqlRequest.Variables = new StjDeserializer().Deserialize(SHORT_JSON); - var gqlRequestJson2 = Serializer.ToJson(gqlRequest); - _httpRequestBody2 = GetHttpRequestBodyFor(gqlRequestJson2); - } + gqlRequest.OperationName = "someOperationName"; + gqlRequest.Variables = new StjDeserializer().Deserialize(SHORT_JSON); + var gqlRequestJson2 = Serializer.ToJson(gqlRequest); + _httpRequestBody2 = GetHttpRequestBodyFor(gqlRequestJson2); + } - private static Stream GetHttpRequestBodyFor(string gqlRequestJson) - { - return new MemoryStream(Encoding.UTF8.GetBytes(gqlRequestJson)); - } + private static Stream GetHttpRequestBodyFor(string gqlRequestJson) + { + return new MemoryStream(Encoding.UTF8.GetBytes(gqlRequestJson)); + } - [IterationSetup] - public void IterationSetup() - { - // Reset stream positions - _httpRequestBody.Position = 0; - _httpRequestBody2.Position = 0; - } + [IterationSetup] + public void IterationSetup() + { + // Reset stream positions + _httpRequestBody.Position = 0; + _httpRequestBody2.Position = 0; + } - [Benchmark(Baseline = true)] - public ValueTask NewtonsoftJson() => _nsjDeserializer.ReadAsync(_httpRequestBody); + [Benchmark(Baseline = true)] + public ValueTask NewtonsoftJson() => _nsjDeserializer.ReadAsync(_httpRequestBody); - [Benchmark] - public ValueTask SystemTextJson() => _stjDeserializer.ReadAsync(_httpRequestBody); + [Benchmark] + public ValueTask SystemTextJson() => _stjDeserializer.ReadAsync(_httpRequestBody); - [Benchmark] - public ValueTask NewtonsoftJson_WithOpNameAndVariables() => _nsjDeserializer.ReadAsync(_httpRequestBody2); + [Benchmark] + public ValueTask NewtonsoftJson_WithOpNameAndVariables() => _nsjDeserializer.ReadAsync(_httpRequestBody2); - [Benchmark] - public ValueTask SystemTextJson_WithOpNameAndVariables() => _stjDeserializer.ReadAsync(_httpRequestBody2); - } + [Benchmark] + public ValueTask SystemTextJson_WithOpNameAndVariables() => _stjDeserializer.ReadAsync(_httpRequestBody2); } diff --git a/src/Benchmarks/Benchmarks/DeserializeInputsFromJsonBenchmark.cs b/src/Benchmarks/Benchmarks/DeserializeInputsFromJsonBenchmark.cs index d178bbeb..a3c2e5a6 100644 --- a/src/Benchmarks/Benchmarks/DeserializeInputsFromJsonBenchmark.cs +++ b/src/Benchmarks/Benchmarks/DeserializeInputsFromJsonBenchmark.cs @@ -2,15 +2,15 @@ using NsjDeserializer = GraphQL.NewtonsoftJson.GraphQLSerializer; using StjDeserializer = GraphQL.SystemTextJson.GraphQLSerializer; -namespace GraphQL.Server.Benchmarks +namespace GraphQL.Server.Benchmarks; + +[MemoryDiagnoser] +[RPlotExporter, CsvMeasurementsExporter] +public class DeserializeInputsFromJsonBenchmark { - [MemoryDiagnoser] - [RPlotExporter, CsvMeasurementsExporter] - public class DeserializeInputsFromJsonBenchmark - { - private NsjDeserializer _nsjDeserializer; - private StjDeserializer _stjDeserializer; - private const string SHORT_JSON = @"{ + private NsjDeserializer _nsjDeserializer; + private StjDeserializer _stjDeserializer; + private const string SHORT_JSON = @"{ ""key0"": null, ""key1"": true, ""key2"": 1.2, @@ -24,22 +24,21 @@ public class DeserializeInputsFromJsonBenchmark } }"; - [GlobalSetup] - public void GlobalSetup() - { - _nsjDeserializer = new NsjDeserializer(s => { }); - _stjDeserializer = new StjDeserializer(s => { }); - } + [GlobalSetup] + public void GlobalSetup() + { + _nsjDeserializer = new NsjDeserializer(s => { }); + _stjDeserializer = new StjDeserializer(s => { }); + } - // Note: There's not a whole lot of value benchmarking these two implementations since their methods just - // call directly to each's underlying core GraphQL repo's `.ToInputs` methods which are essentially benchmarked - // over there by GraphQL.Benchmarks.DeserializationBenchmark. But this does give us the ability to benchmark any - // other custom implementations someone else might want to contribute. + // Note: There's not a whole lot of value benchmarking these two implementations since their methods just + // call directly to each's underlying core GraphQL repo's `.ToInputs` methods which are essentially benchmarked + // over there by GraphQL.Benchmarks.DeserializationBenchmark. But this does give us the ability to benchmark any + // other custom implementations someone else might want to contribute. - [Benchmark(Baseline = true)] - public Inputs NewtonsoftJson() => _nsjDeserializer.Deserialize(SHORT_JSON); + [Benchmark(Baseline = true)] + public Inputs NewtonsoftJson() => _nsjDeserializer.Deserialize(SHORT_JSON); - [Benchmark] - public Inputs SystemTextJson() => _stjDeserializer.Deserialize(SHORT_JSON); - } + [Benchmark] + public Inputs SystemTextJson() => _stjDeserializer.Deserialize(SHORT_JSON); } diff --git a/src/Benchmarks/Program.cs b/src/Benchmarks/Program.cs index 7fde991b..f234aaea 100644 --- a/src/Benchmarks/Program.cs +++ b/src/Benchmarks/Program.cs @@ -1,23 +1,22 @@ using BenchmarkDotNet.Running; -namespace GraphQL.Server.Benchmarks +namespace GraphQL.Server.Benchmarks; + +public class Program { - public class Program + // Call without args to just run the body deserializer benchmark + // Call with an int arg to toggle different benchmarks + public static void Main(string[] args) { - // Call without args to just run the body deserializer benchmark - // Call with an int arg to toggle different benchmarks - public static void Main(string[] args) + if (!args.Any() || !int.TryParse(args[0], out int benchmarkIndex)) { - if (!args.Any() || !int.TryParse(args[0], out int benchmarkIndex)) - { - benchmarkIndex = 0; - } - - _ = benchmarkIndex switch - { - 1 => BenchmarkRunner.Run(), - _ => BenchmarkRunner.Run(), - }; + benchmarkIndex = 0; } + + _ = benchmarkIndex switch + { + 1 => BenchmarkRunner.Run(), + _ => BenchmarkRunner.Run(), + }; } } diff --git a/src/Benchmarks/SchemaIntrospection.cs b/src/Benchmarks/SchemaIntrospection.cs index 755e6f06..8ba041ad 100644 --- a/src/Benchmarks/SchemaIntrospection.cs +++ b/src/Benchmarks/SchemaIntrospection.cs @@ -1,8 +1,8 @@ -namespace GraphQL.Server.Benchmarks +namespace GraphQL.Server.Benchmarks; + +public static class SchemaIntrospection { - public static class SchemaIntrospection - { - public static readonly string IntrospectionQuery = @" + public static readonly string IntrospectionQuery = @" query IntrospectionQuery { __schema { queryType { name } @@ -92,5 +92,4 @@ fragment TypeRef on __Type { } } "; - } } diff --git a/src/Transports.AspNetCore/Extensions/GraphQLBuilderMiddlewareExtensions.cs b/src/Transports.AspNetCore/Extensions/GraphQLBuilderMiddlewareExtensions.cs index a1bf4a09..518882bc 100644 --- a/src/Transports.AspNetCore/Extensions/GraphQLBuilderMiddlewareExtensions.cs +++ b/src/Transports.AspNetCore/Extensions/GraphQLBuilderMiddlewareExtensions.cs @@ -4,26 +4,25 @@ using GraphQL.Server.Transports.AspNetCore; using GraphQL.Types; -namespace GraphQL.Server +namespace GraphQL.Server; + +/// +/// GraphQL specific extension methods for . +/// +public static class GraphQLBuilderMiddlewareExtensions { - /// - /// GraphQL specific extension methods for . - /// - public static class GraphQLBuilderMiddlewareExtensions + public static IGraphQLBuilder AddHttpMiddleware(this IGraphQLBuilder builder) + where TSchema : ISchema { - public static IGraphQLBuilder AddHttpMiddleware(this IGraphQLBuilder builder) - where TSchema : ISchema - { - builder.Services.Register, GraphQLHttpMiddleware>(ServiceLifetime.Singleton); - return builder; - } + builder.Services.Register, GraphQLHttpMiddleware>(ServiceLifetime.Singleton); + return builder; + } - public static IGraphQLBuilder AddHttpMiddleware(this IGraphQLBuilder builder) - where TSchema : ISchema - where TMiddleware : GraphQLHttpMiddleware - { - builder.Services.Register(ServiceLifetime.Singleton); - return builder; - } + public static IGraphQLBuilder AddHttpMiddleware(this IGraphQLBuilder builder) + where TSchema : ISchema + where TMiddleware : GraphQLHttpMiddleware + { + builder.Services.Register(ServiceLifetime.Singleton); + return builder; } } diff --git a/src/Transports.AspNetCore/Extensions/GraphQLBuilderUserContextExtensions.cs b/src/Transports.AspNetCore/Extensions/GraphQLBuilderUserContextExtensions.cs index 78d2f25b..0ca17f24 100644 --- a/src/Transports.AspNetCore/Extensions/GraphQLBuilderUserContextExtensions.cs +++ b/src/Transports.AspNetCore/Extensions/GraphQLBuilderUserContextExtensions.cs @@ -4,90 +4,89 @@ using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; -namespace GraphQL.Server +namespace GraphQL.Server; + +public static class GraphQLBuilderUserContextExtensions { - public static class GraphQLBuilderUserContextExtensions + /// + /// Adds an as a singleton. + /// + /// The type of the implementation. + /// The GraphQL builder. + /// The GraphQL builder. + public static IGraphQLBuilder AddUserContextBuilder(this IGraphQLBuilder builder) + where TUserContextBuilder : class, IUserContextBuilder { - /// - /// Adds an as a singleton. - /// - /// The type of the implementation. - /// The GraphQL builder. - /// The GraphQL builder. - public static IGraphQLBuilder AddUserContextBuilder(this IGraphQLBuilder builder) - where TUserContextBuilder : class, IUserContextBuilder + builder.Services.Register(DI.ServiceLifetime.Singleton); + builder.ConfigureExecutionOptions(async options => { - builder.Services.Register(DI.ServiceLifetime.Singleton); - builder.ConfigureExecutionOptions(async options => + if (options.UserContext == null || options.UserContext.Count == 0 && options.UserContext.GetType() == typeof(Dictionary)) { - if (options.UserContext == null || options.UserContext.Count == 0 && options.UserContext.GetType() == typeof(Dictionary)) - { - var httpContext = options.RequestServices.GetRequiredService().HttpContext; - var contextBuilder = options.RequestServices.GetRequiredService(); - options.UserContext = await contextBuilder.BuildUserContext(httpContext); - } - }); + var httpContext = options.RequestServices.GetRequiredService().HttpContext; + var contextBuilder = options.RequestServices.GetRequiredService(); + options.UserContext = await contextBuilder.BuildUserContext(httpContext); + } + }); - return builder; - } + return builder; + } - /// - /// Set up a delegate to create the UserContext for each GraphQL request - /// - /// - /// The GraphQL builder. - /// A delegate used to create the user context from the . - /// The GraphQL builder. - public static IGraphQLBuilder AddUserContextBuilder(this IGraphQLBuilder builder, Func creator) - where TUserContext : class, IDictionary + /// + /// Set up a delegate to create the UserContext for each GraphQL request + /// + /// + /// The GraphQL builder. + /// A delegate used to create the user context from the . + /// The GraphQL builder. + public static IGraphQLBuilder AddUserContextBuilder(this IGraphQLBuilder builder, Func creator) + where TUserContext : class, IDictionary + { + builder.Services.Register(new UserContextBuilder(creator)); + builder.ConfigureExecutionOptions(options => { - builder.Services.Register(new UserContextBuilder(creator)); - builder.ConfigureExecutionOptions(options => + if (options.UserContext == null || options.UserContext.Count == 0 && options.UserContext.GetType() == typeof(Dictionary)) { - if (options.UserContext == null || options.UserContext.Count == 0 && options.UserContext.GetType() == typeof(Dictionary)) - { - var httpContext = options.RequestServices.GetRequiredService().HttpContext; - options.UserContext = creator(httpContext); - } - }); + var httpContext = options.RequestServices.GetRequiredService().HttpContext; + options.UserContext = creator(httpContext); + } + }); - return builder; - } + return builder; + } - /// - /// Set up a delegate to create the UserContext for each GraphQL request - /// - /// - /// The GraphQL builder. - /// A delegate used to create the user context from the . - /// The GraphQL builder. - public static IGraphQLBuilder AddUserContextBuilder(this IGraphQLBuilder builder, Func> creator) - where TUserContext : class, IDictionary + /// + /// Set up a delegate to create the UserContext for each GraphQL request + /// + /// + /// The GraphQL builder. + /// A delegate used to create the user context from the . + /// The GraphQL builder. + public static IGraphQLBuilder AddUserContextBuilder(this IGraphQLBuilder builder, Func> creator) + where TUserContext : class, IDictionary + { + builder.Services.Register(new UserContextBuilder(creator)); + builder.ConfigureExecutionOptions(async options => { - builder.Services.Register(new UserContextBuilder(creator)); - builder.ConfigureExecutionOptions(async options => + if (options.UserContext == null || options.UserContext.Count == 0 && options.UserContext.GetType() == typeof(Dictionary)) { - if (options.UserContext == null || options.UserContext.Count == 0 && options.UserContext.GetType() == typeof(Dictionary)) - { - var httpContext = options.RequestServices.GetRequiredService().HttpContext; - options.UserContext = await creator(httpContext); - } - }); + var httpContext = options.RequestServices.GetRequiredService().HttpContext; + options.UserContext = await creator(httpContext); + } + }); - return builder; - } + return builder; + } - /// - /// Set up default policy for matching endpoints. It is required when both GraphQL HTTP and - /// GraphQL WebSockets middlewares are mapped to the same endpoint (by default 'graphql'). - /// - /// The GraphQL builder. - /// The GraphQL builder. - public static IGraphQLBuilder AddDefaultEndpointSelectorPolicy(this IGraphQLBuilder builder) - { - builder.Services.TryRegister(DI.ServiceLifetime.Singleton, RegistrationCompareMode.ServiceTypeAndImplementationType); + /// + /// Set up default policy for matching endpoints. It is required when both GraphQL HTTP and + /// GraphQL WebSockets middlewares are mapped to the same endpoint (by default 'graphql'). + /// + /// The GraphQL builder. + /// The GraphQL builder. + public static IGraphQLBuilder AddDefaultEndpointSelectorPolicy(this IGraphQLBuilder builder) + { + builder.Services.TryRegister(DI.ServiceLifetime.Singleton, RegistrationCompareMode.ServiceTypeAndImplementationType); - return builder; - } + return builder; } } diff --git a/src/Transports.AspNetCore/Extensions/GraphQLHttpApplicationBuilderExtensions.cs b/src/Transports.AspNetCore/Extensions/GraphQLHttpApplicationBuilderExtensions.cs index fe5b5a4e..5dd21205 100644 --- a/src/Transports.AspNetCore/Extensions/GraphQLHttpApplicationBuilderExtensions.cs +++ b/src/Transports.AspNetCore/Extensions/GraphQLHttpApplicationBuilderExtensions.cs @@ -2,68 +2,67 @@ using GraphQL.Types; using Microsoft.AspNetCore.Http; -namespace Microsoft.AspNetCore.Builder +namespace Microsoft.AspNetCore.Builder; + +/// +/// Extensions for to add +/// or its descendants in the HTTP request pipeline. +/// +public static class GraphQLHttpApplicationBuilderExtensions { /// - /// Extensions for to add - /// or its descendants in the HTTP request pipeline. + /// Add the GraphQL middleware to the HTTP request pipeline /// - public static class GraphQLHttpApplicationBuilderExtensions - { - /// - /// Add the GraphQL middleware to the HTTP request pipeline - /// - /// The implementation of to use - /// The application builder - /// The path to the GraphQL endpoint which defaults to '/graphql' - /// The received as parameter - public static IApplicationBuilder UseGraphQL(this IApplicationBuilder builder, string path = "/graphql") - where TSchema : ISchema - => builder.UseGraphQL(new PathString(path)); + /// The implementation of to use + /// The application builder + /// The path to the GraphQL endpoint which defaults to '/graphql' + /// The received as parameter + public static IApplicationBuilder UseGraphQL(this IApplicationBuilder builder, string path = "/graphql") + where TSchema : ISchema + => builder.UseGraphQL(new PathString(path)); - /// - /// Add the GraphQL middleware to the HTTP request pipeline - /// - /// The implementation of to use - /// The application builder - /// The path to the GraphQL endpoint - /// The received as parameter - public static IApplicationBuilder UseGraphQL(this IApplicationBuilder builder, PathString path) - where TSchema : ISchema - { - return builder.UseWhen( - context => context.Request.Path.StartsWithSegments(path, out var remaining) && string.IsNullOrEmpty(remaining), - b => b.UseMiddleware>()); - } + /// + /// Add the GraphQL middleware to the HTTP request pipeline + /// + /// The implementation of to use + /// The application builder + /// The path to the GraphQL endpoint + /// The received as parameter + public static IApplicationBuilder UseGraphQL(this IApplicationBuilder builder, PathString path) + where TSchema : ISchema + { + return builder.UseWhen( + context => context.Request.Path.StartsWithSegments(path, out var remaining) && string.IsNullOrEmpty(remaining), + b => b.UseMiddleware>()); + } - /// - /// Add the GraphQL custom middleware to the HTTP request pipeline - /// - /// The implementation of to use - /// Custom middleware inherited from - /// The application builder - /// The path to the GraphQL endpoint which defaults to '/graphql' - /// The received as parameter - public static IApplicationBuilder UseGraphQL(this IApplicationBuilder builder, string path = "/graphql") - where TSchema : ISchema - where TMiddleware : GraphQLHttpMiddleware - => builder.UseGraphQL(new PathString(path)); + /// + /// Add the GraphQL custom middleware to the HTTP request pipeline + /// + /// The implementation of to use + /// Custom middleware inherited from + /// The application builder + /// The path to the GraphQL endpoint which defaults to '/graphql' + /// The received as parameter + public static IApplicationBuilder UseGraphQL(this IApplicationBuilder builder, string path = "/graphql") + where TSchema : ISchema + where TMiddleware : GraphQLHttpMiddleware + => builder.UseGraphQL(new PathString(path)); - /// - /// Add the GraphQL custom middleware to the HTTP request pipeline - /// - /// The implementation of to use - /// Custom middleware inherited from - /// The application builder - /// The path to the GraphQL endpoint - /// The received as parameter - public static IApplicationBuilder UseGraphQL(this IApplicationBuilder builder, PathString path) - where TSchema : ISchema - where TMiddleware : GraphQLHttpMiddleware - { - return builder.UseWhen( - context => context.Request.Path.StartsWithSegments(path, out var remaining) && string.IsNullOrEmpty(remaining), - b => b.UseMiddleware()); - } + /// + /// Add the GraphQL custom middleware to the HTTP request pipeline + /// + /// The implementation of to use + /// Custom middleware inherited from + /// The application builder + /// The path to the GraphQL endpoint + /// The received as parameter + public static IApplicationBuilder UseGraphQL(this IApplicationBuilder builder, PathString path) + where TSchema : ISchema + where TMiddleware : GraphQLHttpMiddleware + { + return builder.UseWhen( + context => context.Request.Path.StartsWithSegments(path, out var remaining) && string.IsNullOrEmpty(remaining), + b => b.UseMiddleware()); } } diff --git a/src/Transports.AspNetCore/Extensions/GraphQLHttpEndpointRouteBuilderExtensions.cs b/src/Transports.AspNetCore/Extensions/GraphQLHttpEndpointRouteBuilderExtensions.cs index d6bfc913..56f359cf 100644 --- a/src/Transports.AspNetCore/Extensions/GraphQLHttpEndpointRouteBuilderExtensions.cs +++ b/src/Transports.AspNetCore/Extensions/GraphQLHttpEndpointRouteBuilderExtensions.cs @@ -2,65 +2,64 @@ using GraphQL.Types; using Microsoft.AspNetCore.Routing; -namespace Microsoft.AspNetCore.Builder +namespace Microsoft.AspNetCore.Builder; + +/// +/// Extensions for to add +/// or its descendants in the HTTP request pipeline. +/// +public static class GraphQLHttpEndpointRouteBuilderExtensions { /// - /// Extensions for to add - /// or its descendants in the HTTP request pipeline. + /// Add the GraphQL middleware to the HTTP request pipeline /// - public static class GraphQLHttpEndpointRouteBuilderExtensions + /// The implementation of to use + /// Defines a contract for a route builder in an application. A route builder specifies the routes for an application. + /// The route pattern. + /// The received as parameter + public static GraphQLHttpEndpointConventionBuilder MapGraphQL(this IEndpointRouteBuilder endpoints, string pattern = "graphql") + where TSchema : ISchema { - /// - /// Add the GraphQL middleware to the HTTP request pipeline - /// - /// The implementation of to use - /// Defines a contract for a route builder in an application. A route builder specifies the routes for an application. - /// The route pattern. - /// The received as parameter - public static GraphQLHttpEndpointConventionBuilder MapGraphQL(this IEndpointRouteBuilder endpoints, string pattern = "graphql") - where TSchema : ISchema - { - if (endpoints == null) - throw new ArgumentNullException(nameof(endpoints)); - - var requestDelegate = endpoints.CreateApplicationBuilder().UseMiddleware>().Build(); - return new GraphQLHttpEndpointConventionBuilder(endpoints.Map(pattern, requestDelegate).WithDisplayName("GraphQL")); - } - - /// - /// Add the GraphQL middleware to the HTTP request pipeline - /// - /// The implementation of to use - /// Custom middleware inherited from - /// Defines a contract for a route builder in an application. A route builder specifies the routes for an application. - /// The route pattern. - /// The received as parameter - public static GraphQLHttpEndpointConventionBuilder MapGraphQL(this IEndpointRouteBuilder endpoints, string pattern = "graphql") - where TSchema : ISchema - where TMiddleware : GraphQLHttpMiddleware - { - if (endpoints == null) - throw new ArgumentNullException(nameof(endpoints)); + if (endpoints == null) + throw new ArgumentNullException(nameof(endpoints)); - var requestDelegate = endpoints.CreateApplicationBuilder().UseMiddleware().Build(); - return new GraphQLHttpEndpointConventionBuilder(endpoints.Map(pattern, requestDelegate).WithDisplayName("GraphQL")); - } + var requestDelegate = endpoints.CreateApplicationBuilder().UseMiddleware>().Build(); + return new GraphQLHttpEndpointConventionBuilder(endpoints.Map(pattern, requestDelegate).WithDisplayName("GraphQL")); } /// - /// Builds conventions that will be used for customization of Microsoft.AspNetCore.Builder.EndpointBuilder instances. - /// Special convention builder that allows you to write specific extension methods for ASP.NET Core routing subsystem. + /// Add the GraphQL middleware to the HTTP request pipeline /// - public class GraphQLHttpEndpointConventionBuilder : IEndpointConventionBuilder + /// The implementation of to use + /// Custom middleware inherited from + /// Defines a contract for a route builder in an application. A route builder specifies the routes for an application. + /// The route pattern. + /// The received as parameter + public static GraphQLHttpEndpointConventionBuilder MapGraphQL(this IEndpointRouteBuilder endpoints, string pattern = "graphql") + where TSchema : ISchema + where TMiddleware : GraphQLHttpMiddleware { - private readonly IEndpointConventionBuilder _builder; + if (endpoints == null) + throw new ArgumentNullException(nameof(endpoints)); - internal GraphQLHttpEndpointConventionBuilder(IEndpointConventionBuilder builder) - { - _builder = builder; - } + var requestDelegate = endpoints.CreateApplicationBuilder().UseMiddleware().Build(); + return new GraphQLHttpEndpointConventionBuilder(endpoints.Map(pattern, requestDelegate).WithDisplayName("GraphQL")); + } +} - /// - public void Add(Action convention) => _builder.Add(convention); +/// +/// Builds conventions that will be used for customization of Microsoft.AspNetCore.Builder.EndpointBuilder instances. +/// Special convention builder that allows you to write specific extension methods for ASP.NET Core routing subsystem. +/// +public class GraphQLHttpEndpointConventionBuilder : IEndpointConventionBuilder +{ + private readonly IEndpointConventionBuilder _builder; + + internal GraphQLHttpEndpointConventionBuilder(IEndpointConventionBuilder builder) + { + _builder = builder; } + + /// + public void Add(Action convention) => _builder.Add(convention); } diff --git a/src/Transports.AspNetCore/GraphQLDefaultEndpointSelectorPolicy.cs b/src/Transports.AspNetCore/GraphQLDefaultEndpointSelectorPolicy.cs index 6492e837..e2f888bc 100644 --- a/src/Transports.AspNetCore/GraphQLDefaultEndpointSelectorPolicy.cs +++ b/src/Transports.AspNetCore/GraphQLDefaultEndpointSelectorPolicy.cs @@ -2,51 +2,50 @@ using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing.Matching; -namespace GraphQL.Server.Transports.AspNetCore +namespace GraphQL.Server.Transports.AspNetCore; + +/// +/// This policy resolves 'Microsoft.AspNetCore.Routing.Matching.AmbiguousMatchException: The request matched multiple endpoints' +/// when both GraphQL HTTP and GraphQL WebSockets middlewares are mapped to the same endpoint (by default 'graphql'). +/// +internal sealed class GraphQLDefaultEndpointSelectorPolicy : MatcherPolicy, IEndpointSelectorPolicy { - /// - /// This policy resolves 'Microsoft.AspNetCore.Routing.Matching.AmbiguousMatchException: The request matched multiple endpoints' - /// when both GraphQL HTTP and GraphQL WebSockets middlewares are mapped to the same endpoint (by default 'graphql'). - /// - internal sealed class GraphQLDefaultEndpointSelectorPolicy : MatcherPolicy, IEndpointSelectorPolicy - { - public override int Order => int.MaxValue; + public override int Order => int.MaxValue; - public bool AppliesToEndpoints(IReadOnlyList endpoints) + public bool AppliesToEndpoints(IReadOnlyList endpoints) + { + for (int i = 0; i < endpoints.Count; ++i) { - for (int i = 0; i < endpoints.Count; ++i) - { - if (endpoints[i].DisplayName == "GraphQL" || endpoints[i].DisplayName == "GraphQL WebSockets") - return true; - } - - return false; + if (endpoints[i].DisplayName == "GraphQL" || endpoints[i].DisplayName == "GraphQL WebSockets") + return true; } - public Task ApplyAsync(HttpContext httpContext, CandidateSet candidates) - { - if (candidates.Count < 2) - return Task.CompletedTask; + return false; + } - for (int i = 0; i < candidates.Count; ++i) - { - if (!candidates.IsValidCandidate(i)) - continue; + public Task ApplyAsync(HttpContext httpContext, CandidateSet candidates) + { + if (candidates.Count < 2) + return Task.CompletedTask; - ref var state = ref candidates[i]; + for (int i = 0; i < candidates.Count; ++i) + { + if (!candidates.IsValidCandidate(i)) + continue; - if (state.Endpoint.DisplayName == "GraphQL" && httpContext.WebSockets.IsWebSocketRequest) - { - candidates.SetValidity(i, false); - } + ref var state = ref candidates[i]; - if (state.Endpoint.DisplayName == "GraphQL WebSockets" && !httpContext.WebSockets.IsWebSocketRequest) - { - candidates.SetValidity(i, false); - } + if (state.Endpoint.DisplayName == "GraphQL" && httpContext.WebSockets.IsWebSocketRequest) + { + candidates.SetValidity(i, false); } - return Task.CompletedTask; + if (state.Endpoint.DisplayName == "GraphQL WebSockets" && !httpContext.WebSockets.IsWebSocketRequest) + { + candidates.SetValidity(i, false); + } } + + return Task.CompletedTask; } } diff --git a/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs b/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs index 74208219..63014f52 100644 --- a/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs +++ b/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs @@ -6,94 +6,119 @@ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; -namespace GraphQL.Server.Transports.AspNetCore +namespace GraphQL.Server.Transports.AspNetCore; + +/// +/// ASP.NET Core middleware for processing GraphQL requests. Can processes both single and batch requests. +/// See Transport-level batching +/// for more information. This middleware useful with and without ASP.NET Core routing. +///

+/// GraphQL over HTTP spec says: +/// GET requests can be used for executing ONLY queries. If the values of query and operationName indicates that +/// a non-query operation is to be executed, the server should immediately respond with an error status code, and +/// halt execution. +///

+/// Attention! The current implementation does not impose such a restriction and allows mutations in GET requests. +///
+/// Type of GraphQL schema that is used to validate and process requests. +public class GraphQLHttpMiddleware : IMiddleware + where TSchema : ISchema { - /// - /// ASP.NET Core middleware for processing GraphQL requests. Can processes both single and batch requests. - /// See Transport-level batching - /// for more information. This middleware useful with and without ASP.NET Core routing. - ///

- /// GraphQL over HTTP spec says: - /// GET requests can be used for executing ONLY queries. If the values of query and operationName indicates that - /// a non-query operation is to be executed, the server should immediately respond with an error status code, and - /// halt execution. - ///

- /// Attention! The current implementation does not impose such a restriction and allows mutations in GET requests. - ///
- /// Type of GraphQL schema that is used to validate and process requests. - public class GraphQLHttpMiddleware : IMiddleware - where TSchema : ISchema - { - private const string DOCS_URL = "See: http://graphql.org/learn/serving-over-http/."; + private const string DOCS_URL = "See: http://graphql.org/learn/serving-over-http/."; + + private readonly IGraphQLTextSerializer _serializer; - private readonly IGraphQLTextSerializer _serializer; + public GraphQLHttpMiddleware(IGraphQLTextSerializer serializer) + { + _serializer = serializer; + } - public GraphQLHttpMiddleware(IGraphQLTextSerializer serializer) + public virtual async Task InvokeAsync(HttpContext context, RequestDelegate next) + { + if (context.WebSockets.IsWebSocketRequest) { - _serializer = serializer; + await next(context); + return; } - public virtual async Task InvokeAsync(HttpContext context, RequestDelegate next) - { - if (context.WebSockets.IsWebSocketRequest) - { - await next(context); - return; - } + // Handle requests as per recommendation at http://graphql.org/learn/serving-over-http/ + // Inspiration: https://github.com/graphql/express-graphql/blob/master/src/index.js + var httpRequest = context.Request; + var httpResponse = context.Response; - // Handle requests as per recommendation at http://graphql.org/learn/serving-over-http/ - // Inspiration: https://github.com/graphql/express-graphql/blob/master/src/index.js - var httpRequest = context.Request; - var httpResponse = context.Response; + var cancellationToken = GetCancellationToken(context); - var cancellationToken = GetCancellationToken(context); + // GraphQL HTTP only supports GET and POST methods + bool isGet = HttpMethods.IsGet(httpRequest.Method); + bool isPost = HttpMethods.IsPost(httpRequest.Method); + if (!isGet && !isPost) + { + httpResponse.Headers["Allow"] = "GET, POST"; + await HandleInvalidHttpMethodErrorAsync(context); + return; + } - // GraphQL HTTP only supports GET and POST methods - bool isGet = HttpMethods.IsGet(httpRequest.Method); - bool isPost = HttpMethods.IsPost(httpRequest.Method); - if (!isGet && !isPost) + // Parse POST body + GraphQLRequest bodyGQLRequest = null; + IList bodyGQLBatchRequest = null; + if (isPost) + { + if (!MediaTypeHeaderValue.TryParse(httpRequest.ContentType, out var mediaTypeHeader)) { - httpResponse.Headers["Allow"] = "GET, POST"; - await HandleInvalidHttpMethodErrorAsync(context); + await HandleContentTypeCouldNotBeParsedErrorAsync(context); return; } - // Parse POST body - GraphQLRequest bodyGQLRequest = null; - IList bodyGQLBatchRequest = null; - if (isPost) + switch (mediaTypeHeader.MediaType) { - if (!MediaTypeHeaderValue.TryParse(httpRequest.ContentType, out var mediaTypeHeader)) - { - await HandleContentTypeCouldNotBeParsedErrorAsync(context); - return; - } - - switch (mediaTypeHeader.MediaType) - { - case MediaType.JSON: - IList deserializationResult; - try - { + case MediaType.JSON: + IList deserializationResult; + try + { #if NET5_0_OR_GREATER - if (!TryGetEncoding(mediaTypeHeader.CharSet, out var sourceEncoding)) - { - await HandleContentTypeCouldNotBeParsedErrorAsync(context); - return; - } - // Wrap content stream into a transcoding stream that buffers the data transcoded from the sourceEncoding to utf-8. - if (sourceEncoding != null && sourceEncoding != System.Text.Encoding.UTF8) - { - using var tempStream = System.Text.Encoding.CreateTranscodingStream(httpRequest.Body, innerStreamEncoding: sourceEncoding, outerStreamEncoding: System.Text.Encoding.UTF8, leaveOpen: true); - deserializationResult = await _serializer.ReadAsync>(tempStream, cancellationToken); - } - else - { - deserializationResult = await _serializer.ReadAsync>(httpRequest.Body, cancellationToken); - } -#else + if (!TryGetEncoding(mediaTypeHeader.CharSet, out var sourceEncoding)) + { + await HandleContentTypeCouldNotBeParsedErrorAsync(context); + return; + } + // Wrap content stream into a transcoding stream that buffers the data transcoded from the sourceEncoding to utf-8. + if (sourceEncoding != null && sourceEncoding != System.Text.Encoding.UTF8) + { + using var tempStream = System.Text.Encoding.CreateTranscodingStream(httpRequest.Body, innerStreamEncoding: sourceEncoding, outerStreamEncoding: System.Text.Encoding.UTF8, leaveOpen: true); + deserializationResult = await _serializer.ReadAsync>(tempStream, cancellationToken); + } + else + { deserializationResult = await _serializer.ReadAsync>(httpRequest.Body, cancellationToken); + } +#else + deserializationResult = await _serializer.ReadAsync>(httpRequest.Body, cancellationToken); #endif + } + catch (Exception ex) + { + if (!await HandleDeserializationErrorAsync(context, ex)) + throw; + return; + } + // https://github.com/graphql-dotnet/server/issues/751 + if (deserializationResult is GraphQLRequest[] array && array.Length == 1) + bodyGQLRequest = deserializationResult[0]; + else + bodyGQLBatchRequest = deserializationResult; + break; + + case MediaType.GRAPH_QL: + bodyGQLRequest = await DeserializeFromGraphBodyAsync(httpRequest.Body); + break; + + default: + if (httpRequest.HasFormContentType) + { + var formCollection = await httpRequest.ReadFormAsync(cancellationToken); + try + { + bodyGQLRequest = DeserializeFromFormBody(formCollection); } catch (Exception ex) { @@ -101,255 +126,229 @@ public virtual async Task InvokeAsync(HttpContext context, RequestDelegate next) throw; return; } - // https://github.com/graphql-dotnet/server/issues/751 - if (deserializationResult is GraphQLRequest[] array && array.Length == 1) - bodyGQLRequest = deserializationResult[0]; - else - bodyGQLBatchRequest = deserializationResult; - break; - - case MediaType.GRAPH_QL: - bodyGQLRequest = await DeserializeFromGraphBodyAsync(httpRequest.Body); break; - - default: - if (httpRequest.HasFormContentType) - { - var formCollection = await httpRequest.ReadFormAsync(cancellationToken); - try - { - bodyGQLRequest = DeserializeFromFormBody(formCollection); - } - catch (Exception ex) - { - if (!await HandleDeserializationErrorAsync(context, ex)) - throw; - return; - } - break; - } - await HandleInvalidContentTypeErrorAsync(context); - return; - } + } + await HandleInvalidContentTypeErrorAsync(context); + return; } + } - // If we don't have a batch request, parse the query from URL too to determine the actual request to run. - // Query string params take priority. - GraphQLRequest gqlRequest = null; - if (bodyGQLBatchRequest == null) + // If we don't have a batch request, parse the query from URL too to determine the actual request to run. + // Query string params take priority. + GraphQLRequest gqlRequest = null; + if (bodyGQLBatchRequest == null) + { + GraphQLRequest urlGQLRequest = null; + try { - GraphQLRequest urlGQLRequest = null; - try - { - urlGQLRequest = DeserializeFromQueryString(httpRequest.Query); - } - catch (Exception ex) - { - if (!await HandleDeserializationErrorAsync(context, ex)) - throw; - return; - } - - gqlRequest = new GraphQLRequest - { - Query = urlGQLRequest.Query ?? bodyGQLRequest?.Query, - Variables = urlGQLRequest.Variables ?? bodyGQLRequest?.Variables, - Extensions = urlGQLRequest.Extensions ?? bodyGQLRequest?.Extensions, - OperationName = urlGQLRequest.OperationName ?? bodyGQLRequest?.OperationName - }; - - if (string.IsNullOrWhiteSpace(gqlRequest.Query)) - { - await HandleNoQueryErrorAsync(context); - return; - } + urlGQLRequest = DeserializeFromQueryString(httpRequest.Query); + } + catch (Exception ex) + { + if (!await HandleDeserializationErrorAsync(context, ex)) + throw; + return; } - // Prepare context and execute - var userContextBuilder = context.RequestServices.GetService(); - var userContext = userContextBuilder == null - ? new Dictionary() // in order to allow resolvers to exchange their state through this object - : await userContextBuilder.BuildUserContext(context); + gqlRequest = new GraphQLRequest + { + Query = urlGQLRequest.Query ?? bodyGQLRequest?.Query, + Variables = urlGQLRequest.Variables ?? bodyGQLRequest?.Variables, + Extensions = urlGQLRequest.Extensions ?? bodyGQLRequest?.Extensions, + OperationName = urlGQLRequest.OperationName ?? bodyGQLRequest?.OperationName + }; - var executer = context.RequestServices.GetRequiredService>(); - await HandleRequestAsync(context, next, userContext, bodyGQLBatchRequest, gqlRequest, executer, cancellationToken); + if (string.IsNullOrWhiteSpace(gqlRequest.Query)) + { + await HandleNoQueryErrorAsync(context); + return; + } } - protected virtual async Task HandleRequestAsync( - HttpContext context, - RequestDelegate next, - IDictionary userContext, - IList bodyGQLBatchRequest, - GraphQLRequest gqlRequest, - IDocumentExecuter executer, - CancellationToken cancellationToken) + // Prepare context and execute + var userContextBuilder = context.RequestServices.GetService(); + var userContext = userContextBuilder == null + ? new Dictionary() // in order to allow resolvers to exchange their state through this object + : await userContextBuilder.BuildUserContext(context); + + var executer = context.RequestServices.GetRequiredService>(); + await HandleRequestAsync(context, next, userContext, bodyGQLBatchRequest, gqlRequest, executer, cancellationToken); + } + + protected virtual async Task HandleRequestAsync( + HttpContext context, + RequestDelegate next, + IDictionary userContext, + IList bodyGQLBatchRequest, + GraphQLRequest gqlRequest, + IDocumentExecuter executer, + CancellationToken cancellationToken) + { + // Normal execution with single graphql request + if (bodyGQLBatchRequest == null) { - // Normal execution with single graphql request - if (bodyGQLBatchRequest == null) - { - var stopwatch = ValueStopwatch.StartNew(); - await RequestExecutingAsync(gqlRequest); - var result = await ExecuteRequestAsync(gqlRequest, userContext, executer, context.RequestServices, cancellationToken); + var stopwatch = ValueStopwatch.StartNew(); + await RequestExecutingAsync(gqlRequest); + var result = await ExecuteRequestAsync(gqlRequest, userContext, executer, context.RequestServices, cancellationToken); - await RequestExecutedAsync(new GraphQLRequestExecutionResult(gqlRequest, result, stopwatch.Elapsed)); + await RequestExecutedAsync(new GraphQLRequestExecutionResult(gqlRequest, result, stopwatch.Elapsed)); - await WriteResponseAsync(context.Response, _serializer, cancellationToken, result); - } - // Execute multiple graphql requests in one batch - else + await WriteResponseAsync(context.Response, _serializer, cancellationToken, result); + } + // Execute multiple graphql requests in one batch + else + { + var executionResults = new ExecutionResult[bodyGQLBatchRequest.Count]; + for (int i = 0; i < bodyGQLBatchRequest.Count; ++i) { - var executionResults = new ExecutionResult[bodyGQLBatchRequest.Count]; - for (int i = 0; i < bodyGQLBatchRequest.Count; ++i) - { - var gqlRequestInBatch = bodyGQLBatchRequest[i]; - - var stopwatch = ValueStopwatch.StartNew(); - await RequestExecutingAsync(gqlRequestInBatch, i); - var result = await ExecuteRequestAsync(gqlRequestInBatch, userContext, executer, context.RequestServices, cancellationToken); + var gqlRequestInBatch = bodyGQLBatchRequest[i]; - await RequestExecutedAsync(new GraphQLRequestExecutionResult(gqlRequestInBatch, result, stopwatch.Elapsed, i)); + var stopwatch = ValueStopwatch.StartNew(); + await RequestExecutingAsync(gqlRequestInBatch, i); + var result = await ExecuteRequestAsync(gqlRequestInBatch, userContext, executer, context.RequestServices, cancellationToken); - executionResults[i] = result; - } + await RequestExecutedAsync(new GraphQLRequestExecutionResult(gqlRequestInBatch, result, stopwatch.Elapsed, i)); - await WriteResponseAsync(context.Response, _serializer, cancellationToken, executionResults); + executionResults[i] = result; } - } - protected virtual async ValueTask HandleDeserializationErrorAsync(HttpContext context, Exception ex) - { - await WriteErrorResponseAsync(context, $"JSON body text could not be parsed. {ex.Message}", HttpStatusCode.BadRequest); - return true; + await WriteResponseAsync(context.Response, _serializer, cancellationToken, executionResults); } + } - protected virtual Task HandleNoQueryErrorAsync(HttpContext context) - => WriteErrorResponseAsync(context, "GraphQL query is missing.", HttpStatusCode.BadRequest); + protected virtual async ValueTask HandleDeserializationErrorAsync(HttpContext context, Exception ex) + { + await WriteErrorResponseAsync(context, $"JSON body text could not be parsed. {ex.Message}", HttpStatusCode.BadRequest); + return true; + } - protected virtual Task HandleContentTypeCouldNotBeParsedErrorAsync(HttpContext context) - => WriteErrorResponseAsync(context, $"Invalid 'Content-Type' header: value '{context.Request.ContentType}' could not be parsed.", HttpStatusCode.UnsupportedMediaType); + protected virtual Task HandleNoQueryErrorAsync(HttpContext context) + => WriteErrorResponseAsync(context, "GraphQL query is missing.", HttpStatusCode.BadRequest); - protected virtual Task HandleInvalidContentTypeErrorAsync(HttpContext context) - => WriteErrorResponseAsync(context, $"Invalid 'Content-Type' header: non-supported media type '{context.Request.ContentType}'. Must be of '{MediaType.JSON}', '{MediaType.GRAPH_QL}' or '{MediaType.FORM}'. {DOCS_URL}", HttpStatusCode.UnsupportedMediaType); + protected virtual Task HandleContentTypeCouldNotBeParsedErrorAsync(HttpContext context) + => WriteErrorResponseAsync(context, $"Invalid 'Content-Type' header: value '{context.Request.ContentType}' could not be parsed.", HttpStatusCode.UnsupportedMediaType); - protected virtual Task HandleInvalidHttpMethodErrorAsync(HttpContext context) - => WriteErrorResponseAsync(context, $"Invalid HTTP method. Only GET and POST are supported. {DOCS_URL}", HttpStatusCode.MethodNotAllowed); + protected virtual Task HandleInvalidContentTypeErrorAsync(HttpContext context) + => WriteErrorResponseAsync(context, $"Invalid 'Content-Type' header: non-supported media type '{context.Request.ContentType}'. Must be of '{MediaType.JSON}', '{MediaType.GRAPH_QL}' or '{MediaType.FORM}'. {DOCS_URL}", HttpStatusCode.UnsupportedMediaType); - protected virtual Task ExecuteRequestAsync( - GraphQLRequest gqlRequest, - IDictionary userContext, - IDocumentExecuter executer, - IServiceProvider requestServices, - CancellationToken token) - => executer.ExecuteAsync(new ExecutionOptions - { - Query = gqlRequest.Query, - OperationName = gqlRequest.OperationName, - Variables = gqlRequest.Variables, - Extensions = gqlRequest.Extensions, - UserContext = userContext, - RequestServices = requestServices, - CancellationToken = token - }); - - protected virtual CancellationToken GetCancellationToken(HttpContext context) => context.RequestAborted; - - protected virtual Task RequestExecutingAsync(GraphQLRequest request, int? indexInBatch = null) - { - // nothing to do in this middleware - return Task.CompletedTask; - } + protected virtual Task HandleInvalidHttpMethodErrorAsync(HttpContext context) + => WriteErrorResponseAsync(context, $"Invalid HTTP method. Only GET and POST are supported. {DOCS_URL}", HttpStatusCode.MethodNotAllowed); - protected virtual Task RequestExecutedAsync(in GraphQLRequestExecutionResult requestExecutionResult) + protected virtual Task ExecuteRequestAsync( + GraphQLRequest gqlRequest, + IDictionary userContext, + IDocumentExecuter executer, + IServiceProvider requestServices, + CancellationToken token) + => executer.ExecuteAsync(new ExecutionOptions { - // nothing to do in this middleware - return Task.CompletedTask; - } + Query = gqlRequest.Query, + OperationName = gqlRequest.OperationName, + Variables = gqlRequest.Variables, + Extensions = gqlRequest.Extensions, + UserContext = userContext, + RequestServices = requestServices, + CancellationToken = token + }); + + protected virtual CancellationToken GetCancellationToken(HttpContext context) => context.RequestAborted; + + protected virtual Task RequestExecutingAsync(GraphQLRequest request, int? indexInBatch = null) + { + // nothing to do in this middleware + return Task.CompletedTask; + } + + protected virtual Task RequestExecutedAsync(in GraphQLRequestExecutionResult requestExecutionResult) + { + // nothing to do in this middleware + return Task.CompletedTask; + } - protected virtual Task WriteErrorResponseAsync(HttpContext context, string errorMessage, HttpStatusCode httpStatusCode) + protected virtual Task WriteErrorResponseAsync(HttpContext context, string errorMessage, HttpStatusCode httpStatusCode) + { + var result = new ExecutionResult { - var result = new ExecutionResult + Errors = new ExecutionErrors { - Errors = new ExecutionErrors - { - new ExecutionError(errorMessage) - } - }; + new ExecutionError(errorMessage) + } + }; - context.Response.ContentType = "application/json"; - context.Response.StatusCode = (int)httpStatusCode; + context.Response.ContentType = "application/json"; + context.Response.StatusCode = (int)httpStatusCode; - return _serializer.WriteAsync(context.Response.Body, result, GetCancellationToken(context)); - } + return _serializer.WriteAsync(context.Response.Body, result, GetCancellationToken(context)); + } - protected virtual Task WriteResponseAsync(HttpResponse httpResponse, IGraphQLSerializer serializer, CancellationToken cancellationToken, TResult result) - { - httpResponse.ContentType = "application/json"; - httpResponse.StatusCode = 200; // OK + protected virtual Task WriteResponseAsync(HttpResponse httpResponse, IGraphQLSerializer serializer, CancellationToken cancellationToken, TResult result) + { + httpResponse.ContentType = "application/json"; + httpResponse.StatusCode = 200; // OK - return serializer.WriteAsync(httpResponse.Body, result, cancellationToken); - } + return serializer.WriteAsync(httpResponse.Body, result, cancellationToken); + } - private const string QUERY_KEY = "query"; - private const string VARIABLES_KEY = "variables"; - private const string EXTENSIONS_KEY = "extensions"; - private const string OPERATION_NAME_KEY = "operationName"; + private const string QUERY_KEY = "query"; + private const string VARIABLES_KEY = "variables"; + private const string EXTENSIONS_KEY = "extensions"; + private const string OPERATION_NAME_KEY = "operationName"; - private GraphQLRequest DeserializeFromQueryString(IQueryCollection queryCollection) => new GraphQLRequest - { - Query = queryCollection.TryGetValue(QUERY_KEY, out var queryValues) ? queryValues[0] : null, - Variables = queryCollection.TryGetValue(VARIABLES_KEY, out var variablesValues) ? _serializer.Deserialize(variablesValues[0]) : null, - Extensions = queryCollection.TryGetValue(EXTENSIONS_KEY, out var extensionsValues) ? _serializer.Deserialize(extensionsValues[0]) : null, - OperationName = queryCollection.TryGetValue(OPERATION_NAME_KEY, out var operationNameValues) ? operationNameValues[0] : null - }; + private GraphQLRequest DeserializeFromQueryString(IQueryCollection queryCollection) => new GraphQLRequest + { + Query = queryCollection.TryGetValue(QUERY_KEY, out var queryValues) ? queryValues[0] : null, + Variables = queryCollection.TryGetValue(VARIABLES_KEY, out var variablesValues) ? _serializer.Deserialize(variablesValues[0]) : null, + Extensions = queryCollection.TryGetValue(EXTENSIONS_KEY, out var extensionsValues) ? _serializer.Deserialize(extensionsValues[0]) : null, + OperationName = queryCollection.TryGetValue(OPERATION_NAME_KEY, out var operationNameValues) ? operationNameValues[0] : null + }; - private GraphQLRequest DeserializeFromFormBody(IFormCollection formCollection) => new GraphQLRequest - { - Query = formCollection.TryGetValue(QUERY_KEY, out var queryValues) ? queryValues[0] : null, - Variables = formCollection.TryGetValue(VARIABLES_KEY, out var variablesValues) ? _serializer.Deserialize(variablesValues[0]) : null, - Extensions = formCollection.TryGetValue(EXTENSIONS_KEY, out var extensionsValues) ? _serializer.Deserialize(extensionsValues[0]) : null, - OperationName = formCollection.TryGetValue(OPERATION_NAME_KEY, out var operationNameValues) ? operationNameValues[0] : null - }; + private GraphQLRequest DeserializeFromFormBody(IFormCollection formCollection) => new GraphQLRequest + { + Query = formCollection.TryGetValue(QUERY_KEY, out var queryValues) ? queryValues[0] : null, + Variables = formCollection.TryGetValue(VARIABLES_KEY, out var variablesValues) ? _serializer.Deserialize(variablesValues[0]) : null, + Extensions = formCollection.TryGetValue(EXTENSIONS_KEY, out var extensionsValues) ? _serializer.Deserialize(extensionsValues[0]) : null, + OperationName = formCollection.TryGetValue(OPERATION_NAME_KEY, out var operationNameValues) ? operationNameValues[0] : null + }; - private async Task DeserializeFromGraphBodyAsync(Stream bodyStream) - { - // In this case, the query is the raw value in the POST body + private async Task DeserializeFromGraphBodyAsync(Stream bodyStream) + { + // In this case, the query is the raw value in the POST body - // Do not explicitly or implicitly (via using, etc.) call dispose because StreamReader will dispose inner stream. - // This leads to the inability to use the stream further by other consumers/middlewares of the request processing - // pipeline. In fact, it is absolutely not dangerous not to dispose StreamReader as it does not perform any useful - // work except for the disposing inner stream. - string query = await new StreamReader(bodyStream).ReadToEndAsync(); + // Do not explicitly or implicitly (via using, etc.) call dispose because StreamReader will dispose inner stream. + // This leads to the inability to use the stream further by other consumers/middlewares of the request processing + // pipeline. In fact, it is absolutely not dangerous not to dispose StreamReader as it does not perform any useful + // work except for the disposing inner stream. + string query = await new StreamReader(bodyStream).ReadToEndAsync(); - return new GraphQLRequest { Query = query }; // application/graphql MediaType supports only query text - } + return new GraphQLRequest { Query = query }; // application/graphql MediaType supports only query text + } #if NET5_0_OR_GREATER - private static bool TryGetEncoding(string charset, out System.Text.Encoding encoding) - { - encoding = null; + private static bool TryGetEncoding(string charset, out System.Text.Encoding encoding) + { + encoding = null; - if (string.IsNullOrEmpty(charset)) - return true; + if (string.IsNullOrEmpty(charset)) + return true; - try + try + { + // Remove at most a single set of quotes. + if (charset.Length > 2 && charset[0] == '\"' && charset[^1] == '\"') { - // Remove at most a single set of quotes. - if (charset.Length > 2 && charset[0] == '\"' && charset[^1] == '\"') - { - encoding = System.Text.Encoding.GetEncoding(charset[1..^1]); - } - else - { - encoding = System.Text.Encoding.GetEncoding(charset); - } + encoding = System.Text.Encoding.GetEncoding(charset[1..^1]); } - catch (ArgumentException) + else { - return false; + encoding = System.Text.Encoding.GetEncoding(charset); } - - return true; } -#endif + catch (ArgumentException) + { + return false; + } + + return true; } +#endif } diff --git a/src/Transports.AspNetCore/GraphQLRequestExecutionResult.cs b/src/Transports.AspNetCore/GraphQLRequestExecutionResult.cs index 7623b9e0..1d39f636 100644 --- a/src/Transports.AspNetCore/GraphQLRequestExecutionResult.cs +++ b/src/Transports.AspNetCore/GraphQLRequestExecutionResult.cs @@ -1,45 +1,44 @@ using GraphQL.Transport; -namespace GraphQL.Server.Transports.AspNetCore +namespace GraphQL.Server.Transports.AspNetCore; + +/// +/// Represents the result of a GraphQL operation. Single GraphQL request may contain several operations, that is, be a batched request. +/// +public readonly struct GraphQLRequestExecutionResult { /// - /// Represents the result of a GraphQL operation. Single GraphQL request may contain several operations, that is, be a batched request. + /// Creates . /// - public readonly struct GraphQLRequestExecutionResult + /// Executed GraphQL request. + /// Result of execution. + /// Elapsed time. + /// Index of the executed request (starting with 0) in case of a batched request, otherwise . + public GraphQLRequestExecutionResult(GraphQLRequest request, ExecutionResult result, TimeSpan elapsed, int? indexInBatch = null) { - /// - /// Creates . - /// - /// Executed GraphQL request. - /// Result of execution. - /// Elapsed time. - /// Index of the executed request (starting with 0) in case of a batched request, otherwise . - public GraphQLRequestExecutionResult(GraphQLRequest request, ExecutionResult result, TimeSpan elapsed, int? indexInBatch = null) - { - Request = request; - Result = result; - Elapsed = elapsed; - IndexInBatch = indexInBatch; - } + Request = request; + Result = result; + Elapsed = elapsed; + IndexInBatch = indexInBatch; + } - /// - /// Executed GraphQL request. - /// - public GraphQLRequest Request { get; } + /// + /// Executed GraphQL request. + /// + public GraphQLRequest Request { get; } - /// - /// Result of execution. - /// - public ExecutionResult Result { get; } + /// + /// Result of execution. + /// + public ExecutionResult Result { get; } - /// - /// Elapsed time. - /// - public TimeSpan Elapsed { get; } + /// + /// Elapsed time. + /// + public TimeSpan Elapsed { get; } - /// - /// Index of the executed request (starting with 0) in case of a batched request, otherwise . - /// - public int? IndexInBatch { get; } - } + /// + /// Index of the executed request (starting with 0) in case of a batched request, otherwise . + /// + public int? IndexInBatch { get; } } diff --git a/src/Transports.AspNetCore/IUserContextBuilder.cs b/src/Transports.AspNetCore/IUserContextBuilder.cs index 34c7c836..f7afa4bc 100644 --- a/src/Transports.AspNetCore/IUserContextBuilder.cs +++ b/src/Transports.AspNetCore/IUserContextBuilder.cs @@ -1,17 +1,16 @@ using Microsoft.AspNetCore.Http; -namespace GraphQL.Server.Transports.AspNetCore +namespace GraphQL.Server.Transports.AspNetCore; + +/// +/// Interface which is responsible of building a UserContext for a GraphQL request +/// +public interface IUserContextBuilder { /// - /// Interface which is responsible of building a UserContext for a GraphQL request + /// Builds the UserContext using the specified /// - public interface IUserContextBuilder - { - /// - /// Builds the UserContext using the specified - /// - /// The for the current request - /// Returns the UserContext - Task> BuildUserContext(HttpContext httpContext); - } + /// The for the current request + /// Returns the UserContext + Task> BuildUserContext(HttpContext httpContext); } diff --git a/src/Transports.AspNetCore/MediaTypes.cs b/src/Transports.AspNetCore/MediaTypes.cs index b2f4b394..f2622ae6 100644 --- a/src/Transports.AspNetCore/MediaTypes.cs +++ b/src/Transports.AspNetCore/MediaTypes.cs @@ -1,9 +1,8 @@ -namespace GraphQL.Server.Transports.AspNetCore +namespace GraphQL.Server.Transports.AspNetCore; + +public static class MediaType { - public static class MediaType - { - public const string JSON = "application/json"; - public const string GRAPH_QL = "application/graphql"; - public const string FORM = "application/x-www-form-urlencoded"; - } + public const string JSON = "application/json"; + public const string GRAPH_QL = "application/graphql"; + public const string FORM = "application/x-www-form-urlencoded"; } diff --git a/src/Transports.AspNetCore/UserContextBuilder.cs b/src/Transports.AspNetCore/UserContextBuilder.cs index a409f458..cc8c845e 100644 --- a/src/Transports.AspNetCore/UserContextBuilder.cs +++ b/src/Transports.AspNetCore/UserContextBuilder.cs @@ -1,25 +1,24 @@ using Microsoft.AspNetCore.Http; -namespace GraphQL.Server.Transports.AspNetCore -{ - public class UserContextBuilder : IUserContextBuilder - where TUserContext : IDictionary - { - private readonly Func> _func; +namespace GraphQL.Server.Transports.AspNetCore; - public UserContextBuilder(Func> func) - { - _func = func ?? throw new ArgumentNullException(nameof(func)); - } +public class UserContextBuilder : IUserContextBuilder + where TUserContext : IDictionary +{ + private readonly Func> _func; - public UserContextBuilder(Func func) - { - if (func == null) - throw new ArgumentNullException(nameof(func)); + public UserContextBuilder(Func> func) + { + _func = func ?? throw new ArgumentNullException(nameof(func)); + } - _func = x => Task.FromResult(func(x)); - } + public UserContextBuilder(Func func) + { + if (func == null) + throw new ArgumentNullException(nameof(func)); - public async Task> BuildUserContext(HttpContext httpContext) => await _func(httpContext); + _func = x => Task.FromResult(func(x)); } + + public async Task> BuildUserContext(HttpContext httpContext) => await _func(httpContext); } diff --git a/src/Transports.Subscriptions.Abstractions/IMessageTransport.cs b/src/Transports.Subscriptions.Abstractions/IMessageTransport.cs index 769f9306..5f4a4263 100644 --- a/src/Transports.Subscriptions.Abstractions/IMessageTransport.cs +++ b/src/Transports.Subscriptions.Abstractions/IMessageTransport.cs @@ -1,19 +1,18 @@ -namespace GraphQL.Server.Transports.Subscriptions.Abstractions +namespace GraphQL.Server.Transports.Subscriptions.Abstractions; + +/// +/// Transport defining the source of the data Reader +/// and target of the data Writer +/// +public interface IMessageTransport { /// - /// Transport defining the source of the data Reader - /// and target of the data Writer + /// Pipeline from which the messages are read /// - public interface IMessageTransport - { - /// - /// Pipeline from which the messages are read - /// - IReaderPipeline Reader { get; } + IReaderPipeline Reader { get; } - /// - /// Pipeline to which the messages are written - /// - IWriterPipeline Writer { get; } - } + /// + /// Pipeline to which the messages are written + /// + IWriterPipeline Writer { get; } } diff --git a/src/Transports.Subscriptions.Abstractions/IOperationMessageListener.cs b/src/Transports.Subscriptions.Abstractions/IOperationMessageListener.cs index 8300fa24..3594fa77 100644 --- a/src/Transports.Subscriptions.Abstractions/IOperationMessageListener.cs +++ b/src/Transports.Subscriptions.Abstractions/IOperationMessageListener.cs @@ -1,22 +1,21 @@ -namespace GraphQL.Server.Transports.Subscriptions.Abstractions +namespace GraphQL.Server.Transports.Subscriptions.Abstractions; + +/// +/// Operation message listener +/// +public interface IOperationMessageListener { + Task BeforeHandleAsync(MessageHandlingContext context); + /// - /// Operation message listener + /// Called to handle message /// - public interface IOperationMessageListener - { - Task BeforeHandleAsync(MessageHandlingContext context); - - /// - /// Called to handle message - /// - /// - Task HandleAsync(MessageHandlingContext context); + /// + Task HandleAsync(MessageHandlingContext context); - /// - /// Called after message has been handled - /// - /// - Task AfterHandleAsync(MessageHandlingContext context); - } + /// + /// Called after message has been handled + /// + /// + Task AfterHandleAsync(MessageHandlingContext context); } diff --git a/src/Transports.Subscriptions.Abstractions/IReaderPipeline.cs b/src/Transports.Subscriptions.Abstractions/IReaderPipeline.cs index a7f3035f..a67a7f24 100644 --- a/src/Transports.Subscriptions.Abstractions/IReaderPipeline.cs +++ b/src/Transports.Subscriptions.Abstractions/IReaderPipeline.cs @@ -1,31 +1,30 @@ using System.Threading.Tasks.Dataflow; using GraphQL.Transport; -namespace GraphQL.Server.Transports.Subscriptions.Abstractions +namespace GraphQL.Server.Transports.Subscriptions.Abstractions; + +/// +/// Pipeline providing the source of messages9 +/// +public interface IReaderPipeline { /// - /// Pipeline providing the source of messages9 + /// Completion task /// - public interface IReaderPipeline - { - /// - /// Completion task - /// - Task Completion { get; } + Task Completion { get; } - /// - /// Link this pipeline to target propagating completion - /// - /// - void LinkTo(ITargetBlock target); + /// + /// Link this pipeline to target propagating completion + /// + /// + void LinkTo(ITargetBlock target); - /// - /// Complete the source of this pipeline - /// - /// - /// Propagates completion from the source to the linked target - /// - /// - Task Complete(); - } + /// + /// Complete the source of this pipeline + /// + /// + /// Propagates completion from the source to the linked target + /// + /// + Task Complete(); } diff --git a/src/Transports.Subscriptions.Abstractions/IServerOperations.cs b/src/Transports.Subscriptions.Abstractions/IServerOperations.cs index 5c7447d0..4346ff91 100644 --- a/src/Transports.Subscriptions.Abstractions/IServerOperations.cs +++ b/src/Transports.Subscriptions.Abstractions/IServerOperations.cs @@ -1,13 +1,12 @@ -namespace GraphQL.Server.Transports.Subscriptions.Abstractions +namespace GraphQL.Server.Transports.Subscriptions.Abstractions; + +public interface IServerOperations //todo: inherit IDisposable { - public interface IServerOperations //todo: inherit IDisposable - { - Task Terminate(); + Task Terminate(); - IReaderPipeline TransportReader { get; } + IReaderPipeline TransportReader { get; } - IWriterPipeline TransportWriter { get; } + IWriterPipeline TransportWriter { get; } - ISubscriptionManager Subscriptions { get; } - } + ISubscriptionManager Subscriptions { get; } } diff --git a/src/Transports.Subscriptions.Abstractions/ISubscriptionManager.cs b/src/Transports.Subscriptions.Abstractions/ISubscriptionManager.cs index efabc14f..d10eacf5 100644 --- a/src/Transports.Subscriptions.Abstractions/ISubscriptionManager.cs +++ b/src/Transports.Subscriptions.Abstractions/ISubscriptionManager.cs @@ -1,26 +1,25 @@ using GraphQL.Transport; -namespace GraphQL.Server.Transports.Subscriptions.Abstractions +namespace GraphQL.Server.Transports.Subscriptions.Abstractions; + +/// +/// Manages operation execution and manages created subscriptions +/// +public interface ISubscriptionManager : IEnumerable //todo: add IDisposable { /// - /// Manages operation execution and manages created subscriptions + /// Execute operation and subscribe if subscription /// - public interface ISubscriptionManager : IEnumerable //todo: add IDisposable - { - /// - /// Execute operation and subscribe if subscription - /// - /// - /// - /// - /// - Task SubscribeOrExecuteAsync(string id, GraphQLRequest payload, MessageHandlingContext context); + /// + /// + /// + /// + Task SubscribeOrExecuteAsync(string id, GraphQLRequest payload, MessageHandlingContext context); - /// - /// Unsubscribe subscription - /// - /// - /// - Task UnsubscribeAsync(string id); - } + /// + /// Unsubscribe subscription + /// + /// + /// + Task UnsubscribeAsync(string id); } diff --git a/src/Transports.Subscriptions.Abstractions/IWriterPipeline.cs b/src/Transports.Subscriptions.Abstractions/IWriterPipeline.cs index 3c5198b7..9894a75e 100644 --- a/src/Transports.Subscriptions.Abstractions/IWriterPipeline.cs +++ b/src/Transports.Subscriptions.Abstractions/IWriterPipeline.cs @@ -1,35 +1,34 @@ using GraphQL.Transport; -namespace GraphQL.Server.Transports.Subscriptions.Abstractions +namespace GraphQL.Server.Transports.Subscriptions.Abstractions; + +/// +/// Pipeline for writing messages +/// +public interface IWriterPipeline { /// - /// Pipeline for writing messages + /// Completion /// - public interface IWriterPipeline - { - /// - /// Completion - /// - Task Completion { get; } + Task Completion { get; } - /// - /// Synchronous write - /// - /// - /// - bool Post(OperationMessage message); + /// + /// Synchronous write + /// + /// + /// + bool Post(OperationMessage message); - /// - /// Asynchronous write - /// - /// - /// - Task SendAsync(OperationMessage message); + /// + /// Asynchronous write + /// + /// + /// + Task SendAsync(OperationMessage message); - /// - /// Complete this pipeline - /// - /// - Task Complete(); - } + /// + /// Complete this pipeline + /// + /// + Task Complete(); } diff --git a/src/Transports.Subscriptions.Abstractions/Internal/ResultHelper.cs b/src/Transports.Subscriptions.Abstractions/Internal/ResultHelper.cs index c2b843bd..9ba0545b 100644 --- a/src/Transports.Subscriptions.Abstractions/Internal/ResultHelper.cs +++ b/src/Transports.Subscriptions.Abstractions/Internal/ResultHelper.cs @@ -1,29 +1,28 @@ using System.Text; -namespace GraphQL.Server.Transports.Subscriptions.Abstractions.Internal +namespace GraphQL.Server.Transports.Subscriptions.Abstractions.Internal; + +internal class ResultHelper { - internal class ResultHelper + public static string GetErrorString(ExecutionResult result) { - public static string GetErrorString(ExecutionResult result) - { - if (result.Errors == null) - return string.Empty; - - var builder = new StringBuilder(); - foreach (var error in result.Errors) - { - builder.AppendLine($"Error: {error.Message}"); + if (result.Errors == null) + return string.Empty; - if (error.Path != null) - builder.AppendLine($"Path: {string.Join(".", error.Path)}"); + var builder = new StringBuilder(); + foreach (var error in result.Errors) + { + builder.AppendLine($"Error: {error.Message}"); - builder.AppendLine("Locations:"); - if (error.Locations != null) - foreach (var location in error.Locations) - builder.AppendLine($"Line: {location.Line} Column: {location.Column}"); - } + if (error.Path != null) + builder.AppendLine($"Path: {string.Join(".", error.Path)}"); - return builder.ToString(); + builder.AppendLine("Locations:"); + if (error.Locations != null) + foreach (var location in error.Locations) + builder.AppendLine($"Line: {location.Line} Column: {location.Column}"); } + + return builder.ToString(); } } diff --git a/src/Transports.Subscriptions.Abstractions/LogMessagesListener.cs b/src/Transports.Subscriptions.Abstractions/LogMessagesListener.cs index 8125f8c6..1375f031 100644 --- a/src/Transports.Subscriptions.Abstractions/LogMessagesListener.cs +++ b/src/Transports.Subscriptions.Abstractions/LogMessagesListener.cs @@ -1,24 +1,23 @@ using Microsoft.Extensions.Logging; -namespace GraphQL.Server.Transports.Subscriptions.Abstractions -{ - public class LogMessagesListener : IOperationMessageListener - { - private readonly ILogger _logger; +namespace GraphQL.Server.Transports.Subscriptions.Abstractions; - public LogMessagesListener(ILogger logger) - { - _logger = logger; - } +public class LogMessagesListener : IOperationMessageListener +{ + private readonly ILogger _logger; - public Task BeforeHandleAsync(MessageHandlingContext context) => Task.FromResult(true); + public LogMessagesListener(ILogger logger) + { + _logger = logger; + } - public Task HandleAsync(MessageHandlingContext context) - { - _logger.LogDebug("Received message: {message}", context.Message); - return Task.CompletedTask; - } + public Task BeforeHandleAsync(MessageHandlingContext context) => Task.FromResult(true); - public Task AfterHandleAsync(MessageHandlingContext context) => Task.CompletedTask; + public Task HandleAsync(MessageHandlingContext context) + { + _logger.LogDebug("Received message: {message}", context.Message); + return Task.CompletedTask; } + + public Task AfterHandleAsync(MessageHandlingContext context) => Task.CompletedTask; } diff --git a/src/Transports.Subscriptions.Abstractions/MessageHandlingContext.cs b/src/Transports.Subscriptions.Abstractions/MessageHandlingContext.cs index 5357021f..4114faf5 100644 --- a/src/Transports.Subscriptions.Abstractions/MessageHandlingContext.cs +++ b/src/Transports.Subscriptions.Abstractions/MessageHandlingContext.cs @@ -1,52 +1,51 @@ using System.Collections.Concurrent; using GraphQL.Transport; -namespace GraphQL.Server.Transports.Subscriptions.Abstractions +namespace GraphQL.Server.Transports.Subscriptions.Abstractions; + +public class MessageHandlingContext : Dictionary, IDisposable { - public class MessageHandlingContext : Dictionary, IDisposable - { - private readonly IServerOperations _server; + private readonly IServerOperations _server; - public MessageHandlingContext(IServerOperations server, OperationMessage message) - { - _server = server; - Reader = server.TransportReader; - Writer = server.TransportWriter; - Subscriptions = server.Subscriptions; - Message = message; - } + public MessageHandlingContext(IServerOperations server, OperationMessage message) + { + _server = server; + Reader = server.TransportReader; + Writer = server.TransportWriter; + Subscriptions = server.Subscriptions; + Message = message; + } - public IReaderPipeline Reader { get; } + public IReaderPipeline Reader { get; } - public IWriterPipeline Writer { get; } + public IWriterPipeline Writer { get; } - public ISubscriptionManager Subscriptions { get; } + public ISubscriptionManager Subscriptions { get; } - public OperationMessage Message { get; } + public OperationMessage Message { get; } - public ConcurrentDictionary Properties { get; protected set; } = new ConcurrentDictionary(); + public ConcurrentDictionary Properties { get; protected set; } = new ConcurrentDictionary(); - public bool Terminated { get; set; } + public bool Terminated { get; set; } - public void Dispose() - { - foreach (var property in Properties) - if (property.Value is IDisposable disposable) - disposable.Dispose(); - } + public void Dispose() + { + foreach (var property in Properties) + if (property.Value is IDisposable disposable) + disposable.Dispose(); + } - public T Get(string key) - { - if (!Properties.TryGetValue(key, out object value)) - return default; + public T Get(string key) + { + if (!Properties.TryGetValue(key, out object value)) + return default; - return value is T variable ? variable : default; - } + return value is T variable ? variable : default; + } - public Task Terminate() - { - Terminated = true; - return _server.Terminate(); - } + public Task Terminate() + { + Terminated = true; + return _server.Terminate(); } } diff --git a/src/Transports.Subscriptions.Abstractions/MessageType.cs b/src/Transports.Subscriptions.Abstractions/MessageType.cs index 2b8f1c45..7d4da0d8 100644 --- a/src/Transports.Subscriptions.Abstractions/MessageType.cs +++ b/src/Transports.Subscriptions.Abstractions/MessageType.cs @@ -1,93 +1,92 @@ using System.Diagnostics.CodeAnalysis; -namespace GraphQL.Server.Transports.Subscriptions.Abstractions +namespace GraphQL.Server.Transports.Subscriptions.Abstractions; + +/// +/// Protocol message types defined in +/// https://github.com/apollographql/subscriptions-transport-ws/blob/master/PROTOCOL.md +/// +[SuppressMessage("ReSharper", "InconsistentNaming")] +public class MessageType { /// - /// Protocol message types defined in - /// https://github.com/apollographql/subscriptions-transport-ws/blob/master/PROTOCOL.md + /// Client sends this message after plain websocket connection to start the communication with the server + /// The server will response only with GQL_CONNECTION_ACK + GQL_CONNECTION_KEEP_ALIVE(if used) or GQL_CONNECTION_ERROR + /// to this message. + /// payload: Object : optional parameters that the client specifies in connectionParams /// - [SuppressMessage("ReSharper", "InconsistentNaming")] - public class MessageType - { - /// - /// Client sends this message after plain websocket connection to start the communication with the server - /// The server will response only with GQL_CONNECTION_ACK + GQL_CONNECTION_KEEP_ALIVE(if used) or GQL_CONNECTION_ERROR - /// to this message. - /// payload: Object : optional parameters that the client specifies in connectionParams - /// - public const string GQL_CONNECTION_INIT = "connection_init"; + public const string GQL_CONNECTION_INIT = "connection_init"; - /// - /// The server may responses with this message to the GQL_CONNECTION_INIT from client, indicates the server accepted - /// the connection. - /// - public const string GQL_CONNECTION_ACK = "connection_ack"; // Server -> Client + /// + /// The server may responses with this message to the GQL_CONNECTION_INIT from client, indicates the server accepted + /// the connection. + /// + public const string GQL_CONNECTION_ACK = "connection_ack"; // Server -> Client - /// - /// The server may responses with this message to the GQL_CONNECTION_INIT from client, indicates the server rejected - /// the connection. - /// It server also respond with this message in case of a parsing errors of the message (which does not disconnect the - /// client, just ignore the message). - /// payload: Object: the server side error - /// - public const string GQL_CONNECTION_ERROR = "connection_error"; // Server -> Client + /// + /// The server may responses with this message to the GQL_CONNECTION_INIT from client, indicates the server rejected + /// the connection. + /// It server also respond with this message in case of a parsing errors of the message (which does not disconnect the + /// client, just ignore the message). + /// payload: Object: the server side error + /// + public const string GQL_CONNECTION_ERROR = "connection_error"; // Server -> Client - /// - /// Server message that should be sent right after each GQL_CONNECTION_ACK processed and then periodically to keep the - /// client connection alive. - /// The client starts to consider the keep alive message only upon the first received keep alive message from the - /// server. - /// - /// NOTE: This one here don't follow the standard due to connection optimization - /// - /// - public const string GQL_CONNECTION_KEEP_ALIVE = "ka"; // Server -> Client + /// + /// Server message that should be sent right after each GQL_CONNECTION_ACK processed and then periodically to keep the + /// client connection alive. + /// The client starts to consider the keep alive message only upon the first received keep alive message from the + /// server. + /// + /// NOTE: This one here don't follow the standard due to connection optimization + /// + /// + public const string GQL_CONNECTION_KEEP_ALIVE = "ka"; // Server -> Client - /// - /// Client sends this message to terminate the connection. - /// - public const string GQL_CONNECTION_TERMINATE = "connection_terminate"; // Client -> Server + /// + /// Client sends this message to terminate the connection. + /// + public const string GQL_CONNECTION_TERMINATE = "connection_terminate"; // Client -> Server - /// - /// Client sends this message to execute GraphQL operation - /// id: string : The id of the GraphQL operation to start - /// payload: Object: - /// query: string : GraphQL operation as string or parsed GraphQL document node - /// variables?: Object : Object with GraphQL variables - /// operationName?: string : GraphQL operation name - /// - public const string GQL_START = "start"; + /// + /// Client sends this message to execute GraphQL operation + /// id: string : The id of the GraphQL operation to start + /// payload: Object: + /// query: string : GraphQL operation as string or parsed GraphQL document node + /// variables?: Object : Object with GraphQL variables + /// operationName?: string : GraphQL operation name + /// + public const string GQL_START = "start"; - /// - /// The server sends this message to transfer the GraphQL execution result from the server to the client, this message - /// is a response for GQL_START message. - /// For each GraphQL operation send with GQL_START, the server will respond with at least one GQL_DATA message. - /// id: string : ID of the operation that was successfully set up - /// payload: Object : - /// data: any: Execution result - /// errors?: Error[] : Array of resolvers errors - /// - public const string GQL_DATA = "data"; // Server -> Client + /// + /// The server sends this message to transfer the GraphQL execution result from the server to the client, this message + /// is a response for GQL_START message. + /// For each GraphQL operation send with GQL_START, the server will respond with at least one GQL_DATA message. + /// id: string : ID of the operation that was successfully set up + /// payload: Object : + /// data: any: Execution result + /// errors?: Error[] : Array of resolvers errors + /// + public const string GQL_DATA = "data"; // Server -> Client - /// - /// Server sends this message upon a failing operation, before the GraphQL execution, usually due to GraphQL validation - /// errors (resolver errors are part of GQL_DATA message, and will be added as errors array) - /// payload: Error : payload with the error attributed to the operation failing on the server - /// id: string : operation ID of the operation that failed on the server - /// - public const string GQL_ERROR = "error"; // Server -> Client + /// + /// Server sends this message upon a failing operation, before the GraphQL execution, usually due to GraphQL validation + /// errors (resolver errors are part of GQL_DATA message, and will be added as errors array) + /// payload: Error : payload with the error attributed to the operation failing on the server + /// id: string : operation ID of the operation that failed on the server + /// + public const string GQL_ERROR = "error"; // Server -> Client - /// - /// Server sends this message to indicate that a GraphQL operation is done, and no more data will arrive for the - /// specific operation. - /// id: string : operation ID of the operation that completed - /// - public const string GQL_COMPLETE = "complete"; // Server -> Client + /// + /// Server sends this message to indicate that a GraphQL operation is done, and no more data will arrive for the + /// specific operation. + /// id: string : operation ID of the operation that completed + /// + public const string GQL_COMPLETE = "complete"; // Server -> Client - /// - /// Client sends this message in order to stop a running GraphQL operation execution (for example: unsubscribe) - /// id: string : operation id - /// - public const string GQL_STOP = "stop"; // Client -> Server - } + /// + /// Client sends this message in order to stop a running GraphQL operation execution (for example: unsubscribe) + /// id: string : operation id + /// + public const string GQL_STOP = "stop"; // Client -> Server } diff --git a/src/Transports.Subscriptions.Abstractions/ProtocolMessageListener.cs b/src/Transports.Subscriptions.Abstractions/ProtocolMessageListener.cs index 51d6d8b3..73652a00 100644 --- a/src/Transports.Subscriptions.Abstractions/ProtocolMessageListener.cs +++ b/src/Transports.Subscriptions.Abstractions/ProtocolMessageListener.cs @@ -1,90 +1,89 @@ using GraphQL.Transport; using Microsoft.Extensions.Logging; -namespace GraphQL.Server.Transports.Subscriptions.Abstractions +namespace GraphQL.Server.Transports.Subscriptions.Abstractions; + +public class ProtocolMessageListener : IOperationMessageListener { - public class ProtocolMessageListener : IOperationMessageListener + private readonly ILogger _logger; + private readonly IGraphQLSerializer _serializer; + + public ProtocolMessageListener(ILogger logger, IGraphQLSerializer serializer) { - private readonly ILogger _logger; - private readonly IGraphQLSerializer _serializer; + _logger = logger; + _serializer = serializer; + } - public ProtocolMessageListener(ILogger logger, IGraphQLSerializer serializer) - { - _logger = logger; - _serializer = serializer; - } + public Task BeforeHandleAsync(MessageHandlingContext context) => Task.CompletedTask; - public Task BeforeHandleAsync(MessageHandlingContext context) => Task.CompletedTask; + public Task HandleAsync(MessageHandlingContext context) + { + if (context.Terminated) + return Task.CompletedTask; - public Task HandleAsync(MessageHandlingContext context) + return context.Message.Type switch { - if (context.Terminated) - return Task.CompletedTask; - - return context.Message.Type switch - { - MessageType.GQL_CONNECTION_INIT => HandleInitAsync(context), - MessageType.GQL_START => HandleStartAsync(context), - MessageType.GQL_STOP => HandleStopAsync(context), - MessageType.GQL_CONNECTION_TERMINATE => HandleTerminateAsync(context), - _ => HandleUnknownAsync(context), - }; - } + MessageType.GQL_CONNECTION_INIT => HandleInitAsync(context), + MessageType.GQL_START => HandleStartAsync(context), + MessageType.GQL_STOP => HandleStopAsync(context), + MessageType.GQL_CONNECTION_TERMINATE => HandleTerminateAsync(context), + _ => HandleUnknownAsync(context), + }; + } - public Task AfterHandleAsync(MessageHandlingContext context) => Task.CompletedTask; + public Task AfterHandleAsync(MessageHandlingContext context) => Task.CompletedTask; - private Task HandleUnknownAsync(MessageHandlingContext context) + private Task HandleUnknownAsync(MessageHandlingContext context) + { + var message = context.Message; + _logger.LogError($"Unexpected message type: {message.Type}"); + return context.Writer.SendAsync(new OperationMessage { - var message = context.Message; - _logger.LogError($"Unexpected message type: {message.Type}"); - return context.Writer.SendAsync(new OperationMessage + Type = MessageType.GQL_CONNECTION_ERROR, + Id = message.Id, + Payload = new ExecutionResult { - Type = MessageType.GQL_CONNECTION_ERROR, - Id = message.Id, - Payload = new ExecutionResult + Errors = new ExecutionErrors { - Errors = new ExecutionErrors - { - new ExecutionError($"Unexpected message type {message.Type}") - } + new ExecutionError($"Unexpected message type {message.Type}") } - }); - } + } + }); + } - private Task HandleStopAsync(MessageHandlingContext context) - { - var message = context.Message; - _logger.LogDebug("Handle stop: {id}", message.Id); - return context.Subscriptions.UnsubscribeAsync(message.Id); - } + private Task HandleStopAsync(MessageHandlingContext context) + { + var message = context.Message; + _logger.LogDebug("Handle stop: {id}", message.Id); + return context.Subscriptions.UnsubscribeAsync(message.Id); + } - private Task HandleStartAsync(MessageHandlingContext context) - { - var message = context.Message; - _logger.LogDebug("Handle start: {id}", message.Id); - var payload = _serializer.ReadNode(message.Payload); - if (payload == null) - throw new InvalidOperationException("Could not get GraphQLRequest from OperationMessage.Payload"); + private Task HandleStartAsync(MessageHandlingContext context) + { + var message = context.Message; + _logger.LogDebug("Handle start: {id}", message.Id); + var payload = _serializer.ReadNode(message.Payload); + if (payload == null) + throw new InvalidOperationException("Could not get GraphQLRequest from OperationMessage.Payload"); - return context.Subscriptions.SubscribeOrExecuteAsync( - message.Id, - payload, - context); - } + return context.Subscriptions.SubscribeOrExecuteAsync( + message.Id, + payload, + context); + } - private Task HandleInitAsync(MessageHandlingContext context) + private Task HandleInitAsync(MessageHandlingContext context) + { + _logger.LogDebug("Handle init"); + return context.Writer.SendAsync(new OperationMessage { - _logger.LogDebug("Handle init"); - return context.Writer.SendAsync(new OperationMessage - { - Type = MessageType.GQL_CONNECTION_ACK - }); - } + Type = MessageType.GQL_CONNECTION_ACK + }); + } - private Task HandleTerminateAsync(MessageHandlingContext context) - { - _logger.LogDebug("Handle terminate"); - return context.Terminate(); - } + private Task HandleTerminateAsync(MessageHandlingContext context) + { + _logger.LogDebug("Handle terminate"); + return context.Terminate(); } } diff --git a/src/Transports.Subscriptions.Abstractions/Subscription.cs b/src/Transports.Subscriptions.Abstractions/Subscription.cs index 9f45f083..0afa7d07 100644 --- a/src/Transports.Subscriptions.Abstractions/Subscription.cs +++ b/src/Transports.Subscriptions.Abstractions/Subscription.cs @@ -5,120 +5,119 @@ using GraphQL.Transport; using Microsoft.Extensions.Logging; -namespace GraphQL.Server.Transports.Subscriptions.Abstractions +namespace GraphQL.Server.Transports.Subscriptions.Abstractions; + +/// +/// Internal observer of the subscription +/// +public class Subscription : IObserver, IDisposable { - /// - /// Internal observer of the subscription - /// - public class Subscription : IObserver, IDisposable + private Action? _completed; + private readonly ILogger _logger; + private IWriterPipeline? _writer; + private IDisposable? _unsubscribe; + + public Subscription(string id, + GraphQLRequest payload, + ExecutionResult result, + IWriterPipeline writer, + Action? completed, + ILogger logger) { - private Action? _completed; - private readonly ILogger _logger; - private IWriterPipeline? _writer; - private IDisposable? _unsubscribe; - - public Subscription(string id, - GraphQLRequest payload, - ExecutionResult result, - IWriterPipeline writer, - Action? completed, - ILogger logger) - { - _writer = writer; - _completed = completed; - _logger = logger; - Id = id; - OriginalPayload = payload; + _writer = writer; + _completed = completed; + _logger = logger; + Id = id; + OriginalPayload = payload; - Subscribe(result); - } + Subscribe(result); + } - public string Id { get; } + public string Id { get; } - public GraphQLRequest OriginalPayload { get; } + public GraphQLRequest OriginalPayload { get; } - public void OnCompleted() + public void OnCompleted() + { + _logger.LogDebug("Subscription: {subscriptionId} completing", Id); + _writer?.Post(new OperationMessage { - _logger.LogDebug("Subscription: {subscriptionId} completing", Id); - _writer?.Post(new OperationMessage - { - Type = MessageType.GQL_COMPLETE, - Id = Id - }); - - _completed?.Invoke(this); - _unsubscribe?.Dispose(); - _completed = null; - _writer = null; - _unsubscribe = null; - } + Type = MessageType.GQL_COMPLETE, + Id = Id + }); + + _completed?.Invoke(this); + _unsubscribe?.Dispose(); + _completed = null; + _writer = null; + _unsubscribe = null; + } - /// - /// Handles errors that are raised from the source, wrapping the error - /// in an instance and sending it to the client. - /// - public void OnError(Exception error) - { - _logger.LogDebug("Subscription: {subscriptionId} got error", Id); - - // exceptions should already be wrapped by the GraphQL engine - if (error is not ExecutionError executionError) - { - // but in the unlikely event that an unhandled exception delegate throws an exception, - // or for any other reason it is not an ExecutionError instance, wrap the error - executionError = new UnhandledError($"Unhandled error of type {error?.GetType().Name}", error!); - } - - // pass along the error as an execution result instance - OnNext(new ExecutionResult { Errors = new ExecutionErrors { executionError } }); - - // Disconnect the client as no additional notification should be received from the source - // - // https://docs.microsoft.com/en-us/dotnet/standard/events/observer-design-pattern-best-practices - // > Once the provider calls the OnError or IObserver.OnCompleted method, there should be - // > no further notifications, and the provider can unsubscribe its observers. - OnCompleted(); - } + /// + /// Handles errors that are raised from the source, wrapping the error + /// in an instance and sending it to the client. + /// + public void OnError(Exception error) + { + _logger.LogDebug("Subscription: {subscriptionId} got error", Id); - public void OnNext(ExecutionResult value) + // exceptions should already be wrapped by the GraphQL engine + if (error is not ExecutionError executionError) { - _logger.LogDebug("Subscription: {subscriptionId} got data", Id); - _writer?.Post(new OperationMessage - { - Type = MessageType.GQL_DATA, - Id = Id, - Payload = value - }); + // but in the unlikely event that an unhandled exception delegate throws an exception, + // or for any other reason it is not an ExecutionError instance, wrap the error + executionError = new UnhandledError($"Unhandled error of type {error?.GetType().Name}", error!); } - public Task UnsubscribeAsync() - { - _logger.LogDebug("Subscription: {subscriptionId} unsubscribing", Id); - _unsubscribe?.Dispose(); - var writer = _writer; - _writer = null; - _unsubscribe = null; - _completed = null; - return writer?.SendAsync(new OperationMessage - { - Type = MessageType.GQL_COMPLETE, - Id = Id - }) ?? Task.CompletedTask; - } + // pass along the error as an execution result instance + OnNext(new ExecutionResult { Errors = new ExecutionErrors { executionError } }); + + // Disconnect the client as no additional notification should be received from the source + // + // https://docs.microsoft.com/en-us/dotnet/standard/events/observer-design-pattern-best-practices + // > Once the provider calls the OnError or IObserver.OnCompleted method, there should be + // > no further notifications, and the provider can unsubscribe its observers. + OnCompleted(); + } - private void Subscribe(ExecutionResult result) + public void OnNext(ExecutionResult value) + { + _logger.LogDebug("Subscription: {subscriptionId} got data", Id); + _writer?.Post(new OperationMessage { - var stream = result.Streams!.Values.Single(); - _unsubscribe = stream.Synchronize().Subscribe(this); - _logger.LogDebug("Subscription: {subscriptionId} subscribed", Id); - } + Type = MessageType.GQL_DATA, + Id = Id, + Payload = value + }); + } - public virtual void Dispose() + public Task UnsubscribeAsync() + { + _logger.LogDebug("Subscription: {subscriptionId} unsubscribing", Id); + _unsubscribe?.Dispose(); + var writer = _writer; + _writer = null; + _unsubscribe = null; + _completed = null; + return writer?.SendAsync(new OperationMessage { - _unsubscribe?.Dispose(); - _unsubscribe = null; - _writer = null; - _completed = null; - } + Type = MessageType.GQL_COMPLETE, + Id = Id + }) ?? Task.CompletedTask; + } + + private void Subscribe(ExecutionResult result) + { + var stream = result.Streams!.Values.Single(); + _unsubscribe = stream.Synchronize().Subscribe(this); + _logger.LogDebug("Subscription: {subscriptionId} subscribed", Id); + } + + public virtual void Dispose() + { + _unsubscribe?.Dispose(); + _unsubscribe = null; + _writer = null; + _completed = null; } } diff --git a/src/Transports.Subscriptions.Abstractions/SubscriptionManager.cs b/src/Transports.Subscriptions.Abstractions/SubscriptionManager.cs index 5067b81a..f17a7738 100644 --- a/src/Transports.Subscriptions.Abstractions/SubscriptionManager.cs +++ b/src/Transports.Subscriptions.Abstractions/SubscriptionManager.cs @@ -5,169 +5,168 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -namespace GraphQL.Server.Transports.Subscriptions.Abstractions +namespace GraphQL.Server.Transports.Subscriptions.Abstractions; + +/// +public class SubscriptionManager : ISubscriptionManager, IDisposable { - /// - public class SubscriptionManager : ISubscriptionManager, IDisposable - { - private readonly IDocumentExecuter _executer; + private readonly IDocumentExecuter _executer; - private readonly ILogger _logger; - private readonly IServiceScopeFactory _serviceScopeFactory; - private readonly ILoggerFactory _loggerFactory; - private volatile bool _disposed; + private readonly ILogger _logger; + private readonly IServiceScopeFactory _serviceScopeFactory; + private readonly ILoggerFactory _loggerFactory; + private volatile bool _disposed; - private readonly ConcurrentDictionary _subscriptions = new(); + private readonly ConcurrentDictionary _subscriptions = new(); - public SubscriptionManager(IDocumentExecuter executer, ILoggerFactory loggerFactory, IServiceScopeFactory serviceScopeFactory) - { - _executer = executer; - _loggerFactory = loggerFactory; - _logger = loggerFactory.CreateLogger(); - _serviceScopeFactory = serviceScopeFactory; - } + public SubscriptionManager(IDocumentExecuter executer, ILoggerFactory loggerFactory, IServiceScopeFactory serviceScopeFactory) + { + _executer = executer; + _loggerFactory = loggerFactory; + _logger = loggerFactory.CreateLogger(); + _serviceScopeFactory = serviceScopeFactory; + } - public Subscription this[string id] => _subscriptions[id]; + public Subscription this[string id] => _subscriptions[id]; - public IEnumerator GetEnumerator() => _subscriptions.Values.GetEnumerator(); + public IEnumerator GetEnumerator() => _subscriptions.Values.GetEnumerator(); - /// - public async Task SubscribeOrExecuteAsync( - string id, - GraphQLRequest payload, - MessageHandlingContext context) - { - if (id == null) - throw new ArgumentNullException(nameof(id)); - if (payload == null) - throw new ArgumentNullException(nameof(payload)); - if (context == null) - throw new ArgumentNullException(nameof(context)); - if (_disposed) - throw new ObjectDisposedException(nameof(SubscriptionManager)); - - var subscription = await ExecuteAsync(id, payload, context).ConfigureAwait(false); - - if (subscription == null) - return; - - if (_disposed) - subscription.Dispose(); - else - _subscriptions[id] = subscription; - } + /// + public async Task SubscribeOrExecuteAsync( + string id, + GraphQLRequest payload, + MessageHandlingContext context) + { + if (id == null) + throw new ArgumentNullException(nameof(id)); + if (payload == null) + throw new ArgumentNullException(nameof(payload)); + if (context == null) + throw new ArgumentNullException(nameof(context)); + if (_disposed) + throw new ObjectDisposedException(nameof(SubscriptionManager)); + + var subscription = await ExecuteAsync(id, payload, context).ConfigureAwait(false); + + if (subscription == null) + return; + + if (_disposed) + subscription.Dispose(); + else + _subscriptions[id] = subscription; + } - /// - public Task UnsubscribeAsync(string id) - { - if (_subscriptions.TryRemove(id, out var removed)) - return removed.UnsubscribeAsync(); + /// + public Task UnsubscribeAsync(string id) + { + if (_subscriptions.TryRemove(id, out var removed)) + return removed.UnsubscribeAsync(); - _logger.LogDebug("Subscription: {subscriptionId} unsubscribed", id); - return Task.CompletedTask; - } + _logger.LogDebug("Subscription: {subscriptionId} unsubscribed", id); + return Task.CompletedTask; + } - IEnumerator IEnumerable.GetEnumerator() => _subscriptions.Values.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => _subscriptions.Values.GetEnumerator(); - private async Task ExecuteAsync( - string id, - GraphQLRequest payload, - MessageHandlingContext context) - { - var writer = context.Writer; - _logger.LogDebug("Executing operation: {operationName} query: {query}", - payload.OperationName, - payload.Query); + private async Task ExecuteAsync( + string id, + GraphQLRequest payload, + MessageHandlingContext context) + { + var writer = context.Writer; + _logger.LogDebug("Executing operation: {operationName} query: {query}", + payload.OperationName, + payload.Query); - ExecutionResult result; - using (var scope = _serviceScopeFactory.CreateScope()) + ExecutionResult result; + using (var scope = _serviceScopeFactory.CreateScope()) + { + result = await _executer.ExecuteAsync(new ExecutionOptions { - result = await _executer.ExecuteAsync(new ExecutionOptions - { - Query = payload.Query, - OperationName = payload.OperationName, - Variables = payload.Variables, - Extensions = payload.Extensions, - RequestServices = scope.ServiceProvider, - UserContext = context, - }).ConfigureAwait(false); - } + Query = payload.Query, + OperationName = payload.OperationName, + Variables = payload.Variables, + Extensions = payload.Extensions, + RequestServices = scope.ServiceProvider, + UserContext = context, + }).ConfigureAwait(false); + } - if (result.Errors != null && result.Errors.Any()) + if (result.Errors != null && result.Errors.Any()) + { + _logger.LogError("Execution errors: {errors}", ResultHelper.GetErrorString(result)); + await writer.SendAsync(new OperationMessage { - _logger.LogError("Execution errors: {errors}", ResultHelper.GetErrorString(result)); - await writer.SendAsync(new OperationMessage - { - Type = MessageType.GQL_ERROR, - Id = id, - Payload = result - }).ConfigureAwait(false); + Type = MessageType.GQL_ERROR, + Id = id, + Payload = result + }).ConfigureAwait(false); - return null; - } + return null; + } - // is sub - if (result.Streams != null) - using (_logger.BeginScope("Subscribing to: {subscriptionId}", id)) + // is sub + if (result.Streams != null) + using (_logger.BeginScope("Subscribing to: {subscriptionId}", id)) + { + if (result.Streams?.Values.SingleOrDefault() == null) { - if (result.Streams?.Values.SingleOrDefault() == null) + _logger.LogError("Cannot subscribe as no result stream available"); + await writer.SendAsync(new OperationMessage { - _logger.LogError("Cannot subscribe as no result stream available"); - await writer.SendAsync(new OperationMessage - { - Type = MessageType.GQL_ERROR, - Id = id, - Payload = result - }).ConfigureAwait(false); - - return null; - } + Type = MessageType.GQL_ERROR, + Id = id, + Payload = result + }).ConfigureAwait(false); - _logger.LogDebug("Creating subscription"); - return new Subscription( - id, - payload, - result, - writer, - sub => _subscriptions.TryRemove(id, out _), - _loggerFactory.CreateLogger()); + return null; } - //is query or mutation - await writer.SendAsync(new OperationMessage - { - Type = MessageType.GQL_DATA, - Id = id, - Payload = result - }).ConfigureAwait(false); + _logger.LogDebug("Creating subscription"); + return new Subscription( + id, + payload, + result, + writer, + sub => _subscriptions.TryRemove(id, out _), + _loggerFactory.CreateLogger()); + } - await writer.SendAsync(new OperationMessage - { - Type = MessageType.GQL_COMPLETE, - Id = id - }).ConfigureAwait(false); + //is query or mutation + await writer.SendAsync(new OperationMessage + { + Type = MessageType.GQL_DATA, + Id = id, + Payload = result + }).ConfigureAwait(false); - return null; - } + await writer.SendAsync(new OperationMessage + { + Type = MessageType.GQL_COMPLETE, + Id = id + }).ConfigureAwait(false); + + return null; + } - public virtual void Dispose() + public virtual void Dispose() + { + _disposed = true; + while (_subscriptions.Count > 0) { - _disposed = true; - while (_subscriptions.Count > 0) + var subscriptions = _subscriptions.ToArray(); + foreach (var subscriptionPair in subscriptions) { - var subscriptions = _subscriptions.ToArray(); - foreach (var subscriptionPair in subscriptions) + if (_subscriptions.TryRemove(subscriptionPair.Key, out var subscription)) { - if (_subscriptions.TryRemove(subscriptionPair.Key, out var subscription)) + try + { + subscription.Dispose(); + } + catch (Exception ex) { - try - { - subscription.Dispose(); - } - catch (Exception ex) - { - _logger.LogError($"Failed to dispose subscription '{subscriptionPair.Key}': ${ex}"); - } + _logger.LogError($"Failed to dispose subscription '{subscriptionPair.Key}': ${ex}"); } } } diff --git a/src/Transports.Subscriptions.Abstractions/SubscriptionServer.cs b/src/Transports.Subscriptions.Abstractions/SubscriptionServer.cs index 87ec374e..cef9c036 100644 --- a/src/Transports.Subscriptions.Abstractions/SubscriptionServer.cs +++ b/src/Transports.Subscriptions.Abstractions/SubscriptionServer.cs @@ -3,139 +3,138 @@ using GraphQL.Transport; using Microsoft.Extensions.Logging; -namespace GraphQL.Server.Transports.Subscriptions.Abstractions +namespace GraphQL.Server.Transports.Subscriptions.Abstractions; + +/// +/// Subscription server +/// Acts as a message pump reading, handling and writing messages +/// +public class SubscriptionServer : IServerOperations, IDisposable { - /// - /// Subscription server - /// Acts as a message pump reading, handling and writing messages - /// - public class SubscriptionServer : IServerOperations, IDisposable + private readonly ILogger _logger; + private readonly IEnumerable _messageListeners; + private ActionBlock _handler; + + public SubscriptionServer( + IMessageTransport transport, + ISubscriptionManager subscriptions, + IEnumerable messageListeners, + ILogger logger) { - private readonly ILogger _logger; - private readonly IEnumerable _messageListeners; - private ActionBlock _handler; - - public SubscriptionServer( - IMessageTransport transport, - ISubscriptionManager subscriptions, - IEnumerable messageListeners, - ILogger logger) - { - _messageListeners = messageListeners; - _logger = logger; - Subscriptions = subscriptions; - Transport = transport; - } + _messageListeners = messageListeners; + _logger = logger; + Subscriptions = subscriptions; + Transport = transport; + } - public IMessageTransport Transport { get; } + public IMessageTransport Transport { get; } - public ISubscriptionManager Subscriptions { get; } + public ISubscriptionManager Subscriptions { get; } - public IReaderPipeline TransportReader { get; set; } + public IReaderPipeline TransportReader { get; set; } - public IWriterPipeline TransportWriter { get; set; } + public IWriterPipeline TransportWriter { get; set; } - public async Task OnConnect() - { - _logger.LogDebug("Connected..."); - LinkToTransportWriter(); - LinkToTransportReader(); + public async Task OnConnect() + { + _logger.LogDebug("Connected..."); + LinkToTransportWriter(); + LinkToTransportReader(); - LogServerInformation(); + LogServerInformation(); - // when transport reader is completed it should propagate here - await _handler.Completion.ConfigureAwait(false); + // when transport reader is completed it should propagate here + await _handler.Completion.ConfigureAwait(false); - // complete write buffer - await TransportWriter.Complete().ConfigureAwait(false); - await TransportWriter.Completion.ConfigureAwait(false); - } + // complete write buffer + await TransportWriter.Complete().ConfigureAwait(false); + await TransportWriter.Completion.ConfigureAwait(false); + } - public Task OnDisconnect() => Terminate(); + public Task OnDisconnect() => Terminate(); - public async Task Terminate() - { - foreach (var subscription in Subscriptions) - await Subscriptions.UnsubscribeAsync(subscription.Id).ConfigureAwait(false); + public async Task Terminate() + { + foreach (var subscription in Subscriptions) + await Subscriptions.UnsubscribeAsync(subscription.Id).ConfigureAwait(false); - // this should propagate to handler completion - await TransportReader.Complete().ConfigureAwait(false); - } + // this should propagate to handler completion + await TransportReader.Complete().ConfigureAwait(false); + } - private void LogServerInformation() - { - // list listeners - var builder = new StringBuilder(); - builder.AppendLine("Message listeners:"); - foreach (var listener in _messageListeners) - builder.AppendLine(listener.GetType().FullName); + private void LogServerInformation() + { + // list listeners + var builder = new StringBuilder(); + builder.AppendLine("Message listeners:"); + foreach (var listener in _messageListeners) + builder.AppendLine(listener.GetType().FullName); - _logger.LogDebug(builder.ToString()); - } + _logger.LogDebug(builder.ToString()); + } - private void LinkToTransportReader() + private void LinkToTransportReader() + { + _logger.LogDebug("Creating reader pipeline"); + TransportReader = Transport.Reader; + _handler = new ActionBlock(HandleMessageAsync, new ExecutionDataflowBlockOptions { - _logger.LogDebug("Creating reader pipeline"); - TransportReader = Transport.Reader; - _handler = new ActionBlock(HandleMessageAsync, new ExecutionDataflowBlockOptions - { - EnsureOrdered = true, - BoundedCapacity = 1 - }); - - TransportReader.LinkTo(_handler); - _logger.LogDebug("Reader pipeline created"); - } + EnsureOrdered = true, + BoundedCapacity = 1 + }); + + TransportReader.LinkTo(_handler); + _logger.LogDebug("Reader pipeline created"); + } - private async Task HandleMessageAsync(OperationMessage message) + private async Task HandleMessageAsync(OperationMessage message) + { + _logger.LogDebug("Handling message: {id} of type: {type}", message.Id, message.Type); + using (var context = await BuildMessageHandlingContext(message).ConfigureAwait(false)) { - _logger.LogDebug("Handling message: {id} of type: {type}", message.Id, message.Type); - using (var context = await BuildMessageHandlingContext(message).ConfigureAwait(false)) - { - await OnBeforeHandleAsync(context).ConfigureAwait(false); + await OnBeforeHandleAsync(context).ConfigureAwait(false); - if (context.Terminated) - return; + if (context.Terminated) + return; - await OnHandleAsync(context).ConfigureAwait(false); - await OnAfterHandleAsync(context).ConfigureAwait(false); - } + await OnHandleAsync(context).ConfigureAwait(false); + await OnAfterHandleAsync(context).ConfigureAwait(false); } + } - private async Task OnBeforeHandleAsync(MessageHandlingContext context) + private async Task OnBeforeHandleAsync(MessageHandlingContext context) + { + foreach (var listener in _messageListeners) { - foreach (var listener in _messageListeners) - { - await listener.BeforeHandleAsync(context).ConfigureAwait(false); - } + await listener.BeforeHandleAsync(context).ConfigureAwait(false); } + } - private Task BuildMessageHandlingContext(OperationMessage message) - => Task.FromResult(new MessageHandlingContext(this, message)); - - private async Task OnHandleAsync(MessageHandlingContext context) - { - foreach (var listener in _messageListeners) - { - await listener.HandleAsync(context).ConfigureAwait(false); - } - } + private Task BuildMessageHandlingContext(OperationMessage message) + => Task.FromResult(new MessageHandlingContext(this, message)); - private async Task OnAfterHandleAsync(MessageHandlingContext context) + private async Task OnHandleAsync(MessageHandlingContext context) + { + foreach (var listener in _messageListeners) { - foreach (var listener in _messageListeners) - { - await listener.AfterHandleAsync(context).ConfigureAwait(false); - } + await listener.HandleAsync(context).ConfigureAwait(false); } + } - private void LinkToTransportWriter() + private async Task OnAfterHandleAsync(MessageHandlingContext context) + { + foreach (var listener in _messageListeners) { - _logger.LogDebug("Creating writer pipeline"); - TransportWriter = Transport.Writer; - _logger.LogDebug("Writer pipeline created"); + await listener.AfterHandleAsync(context).ConfigureAwait(false); } + } - public virtual void Dispose() => (Subscriptions as IDisposable)?.Dispose(); + private void LinkToTransportWriter() + { + _logger.LogDebug("Creating writer pipeline"); + TransportWriter = Transport.Writer; + _logger.LogDebug("Writer pipeline created"); } + + public virtual void Dispose() => (Subscriptions as IDisposable)?.Dispose(); } diff --git a/src/Transports.Subscriptions.WebSockets/Extensions/GraphQLBuilderWebSocketsExtensions.cs b/src/Transports.Subscriptions.WebSockets/Extensions/GraphQLBuilderWebSocketsExtensions.cs index 4160bde6..0de3fe56 100644 --- a/src/Transports.Subscriptions.WebSockets/Extensions/GraphQLBuilderWebSocketsExtensions.cs +++ b/src/Transports.Subscriptions.WebSockets/Extensions/GraphQLBuilderWebSocketsExtensions.cs @@ -3,36 +3,35 @@ using GraphQL.Server.Transports.WebSockets; using GraphQL.Types; -namespace GraphQL.Server +namespace GraphQL.Server; + +public static class GraphQLBuilderWebSocketsExtensions { - public static class GraphQLBuilderWebSocketsExtensions + /// + /// Add required services for GraphQL web sockets + /// + public static IGraphQLBuilder AddWebSockets(this IGraphQLBuilder builder) { - /// - /// Add required services for GraphQL web sockets - /// - public static IGraphQLBuilder AddWebSockets(this IGraphQLBuilder builder) - { - builder.Services - .Register(typeof(IWebSocketConnectionFactory<>), typeof(WebSocketConnectionFactory<>), ServiceLifetime.Transient) - .Register(ServiceLifetime.Transient) - .Register(ServiceLifetime.Transient); + builder.Services + .Register(typeof(IWebSocketConnectionFactory<>), typeof(WebSocketConnectionFactory<>), ServiceLifetime.Transient) + .Register(ServiceLifetime.Transient) + .Register(ServiceLifetime.Transient); - return builder; - } + return builder; + } - public static IGraphQLBuilder AddWebSocketsHttpMiddleware(this IGraphQLBuilder builder) - where TSchema : ISchema - { - builder.Services.Register, GraphQLWebSocketsMiddleware>(ServiceLifetime.Singleton); - return builder; - } + public static IGraphQLBuilder AddWebSocketsHttpMiddleware(this IGraphQLBuilder builder) + where TSchema : ISchema + { + builder.Services.Register, GraphQLWebSocketsMiddleware>(ServiceLifetime.Singleton); + return builder; + } - public static IGraphQLBuilder AddWebSocketsHttpMiddleware(this IGraphQLBuilder builder) - where TSchema : ISchema - where TMiddleware : GraphQLWebSocketsMiddleware - { - builder.Services.Register(ServiceLifetime.Singleton); - return builder; - } + public static IGraphQLBuilder AddWebSocketsHttpMiddleware(this IGraphQLBuilder builder) + where TSchema : ISchema + where TMiddleware : GraphQLWebSocketsMiddleware + { + builder.Services.Register(ServiceLifetime.Singleton); + return builder; } } diff --git a/src/Transports.Subscriptions.WebSockets/Extensions/GraphQLWebSocketsApplicationBuilderExtensions.cs b/src/Transports.Subscriptions.WebSockets/Extensions/GraphQLWebSocketsApplicationBuilderExtensions.cs index 43119b92..3515d569 100644 --- a/src/Transports.Subscriptions.WebSockets/Extensions/GraphQLWebSocketsApplicationBuilderExtensions.cs +++ b/src/Transports.Subscriptions.WebSockets/Extensions/GraphQLWebSocketsApplicationBuilderExtensions.cs @@ -2,42 +2,41 @@ using GraphQL.Types; using Microsoft.AspNetCore.Http; -namespace Microsoft.AspNetCore.Builder +namespace Microsoft.AspNetCore.Builder; + +/// +/// Extensions for to add +/// or its descendants in the HTTP request pipeline. +/// +public static class GraphQLWebSocketsApplicationBuilderExtensions { /// - /// Extensions for to add - /// or its descendants in the HTTP request pipeline. + /// Add GraphQL web sockets middleware to the request pipeline /// - public static class GraphQLWebSocketsApplicationBuilderExtensions - { - /// - /// Add GraphQL web sockets middleware to the request pipeline - /// - /// The implementation of to use - /// The application builder - /// The path to the GraphQL web socket endpoint which defaults to '/graphql' - /// The received as parameter - public static IApplicationBuilder UseGraphQLWebSockets( - this IApplicationBuilder builder, - string path = "/graphql") - where TSchema : ISchema - => builder.UseGraphQLWebSockets(new PathString(path)); + /// The implementation of to use + /// The application builder + /// The path to the GraphQL web socket endpoint which defaults to '/graphql' + /// The received as parameter + public static IApplicationBuilder UseGraphQLWebSockets( + this IApplicationBuilder builder, + string path = "/graphql") + where TSchema : ISchema + => builder.UseGraphQLWebSockets(new PathString(path)); - /// - /// Add GraphQL web sockets middleware to the request pipeline - /// - /// The implementation of to use - /// The application builder - /// The path to the GraphQL endpoint - /// The received as parameter - public static IApplicationBuilder UseGraphQLWebSockets( - this IApplicationBuilder builder, - PathString path) - where TSchema : ISchema - { - return builder.UseWhen( - context => context.Request.Path.StartsWithSegments(path, out var remaining) && string.IsNullOrEmpty(remaining), - b => b.UseMiddleware>()); - } + /// + /// Add GraphQL web sockets middleware to the request pipeline + /// + /// The implementation of to use + /// The application builder + /// The path to the GraphQL endpoint + /// The received as parameter + public static IApplicationBuilder UseGraphQLWebSockets( + this IApplicationBuilder builder, + PathString path) + where TSchema : ISchema + { + return builder.UseWhen( + context => context.Request.Path.StartsWithSegments(path, out var remaining) && string.IsNullOrEmpty(remaining), + b => b.UseMiddleware>()); } } diff --git a/src/Transports.Subscriptions.WebSockets/Extensions/GraphQLWebSocketsEndpointRouteBuilderExtensions.cs b/src/Transports.Subscriptions.WebSockets/Extensions/GraphQLWebSocketsEndpointRouteBuilderExtensions.cs index 07f73e18..a40aa435 100644 --- a/src/Transports.Subscriptions.WebSockets/Extensions/GraphQLWebSocketsEndpointRouteBuilderExtensions.cs +++ b/src/Transports.Subscriptions.WebSockets/Extensions/GraphQLWebSocketsEndpointRouteBuilderExtensions.cs @@ -2,65 +2,64 @@ using GraphQL.Types; using Microsoft.AspNetCore.Routing; -namespace Microsoft.AspNetCore.Builder +namespace Microsoft.AspNetCore.Builder; + +/// +/// Extensions for to add +/// or its descendants in the HTTP request pipeline. +/// +public static class GraphQLWebSocketsEndpointRouteBuilderExtensions { /// - /// Extensions for to add - /// or its descendants in the HTTP request pipeline. + /// Add the GraphQL web sockets middleware to the HTTP request pipeline /// - public static class GraphQLWebSocketsEndpointRouteBuilderExtensions + /// The implementation of to use + /// Defines a contract for a route builder in an application. A route builder specifies the routes for an application. + /// The route pattern. + /// The received as parameter + public static GraphQLWebSocketsEndpointConventionBuilder MapGraphQLWebSockets(this IEndpointRouteBuilder endpoints, string pattern = "graphql") + where TSchema : ISchema { - /// - /// Add the GraphQL web sockets middleware to the HTTP request pipeline - /// - /// The implementation of to use - /// Defines a contract for a route builder in an application. A route builder specifies the routes for an application. - /// The route pattern. - /// The received as parameter - public static GraphQLWebSocketsEndpointConventionBuilder MapGraphQLWebSockets(this IEndpointRouteBuilder endpoints, string pattern = "graphql") - where TSchema : ISchema - { - if (endpoints == null) - throw new ArgumentNullException(nameof(endpoints)); - - var requestDelegate = endpoints.CreateApplicationBuilder().UseMiddleware>().Build(); - return new GraphQLWebSocketsEndpointConventionBuilder(endpoints.Map(pattern, requestDelegate).WithDisplayName("GraphQL WebSockets")); - } - - /// - /// Add the GraphQL web sockets middleware to the HTTP request pipeline - /// - /// The implementation of to use - /// Custom middleware inherited from - /// Defines a contract for a route builder in an application. A route builder specifies the routes for an application. - /// The route pattern. - /// The received as parameter - public static GraphQLWebSocketsEndpointConventionBuilder MapGraphQLWebSockets(this IEndpointRouteBuilder endpoints, string pattern = "graphql") - where TSchema : ISchema - where TMiddleware : GraphQLWebSocketsMiddleware - { - if (endpoints == null) - throw new ArgumentNullException(nameof(endpoints)); + if (endpoints == null) + throw new ArgumentNullException(nameof(endpoints)); - var requestDelegate = endpoints.CreateApplicationBuilder().UseMiddleware().Build(); - return new GraphQLWebSocketsEndpointConventionBuilder(endpoints.Map(pattern, requestDelegate).WithDisplayName("GraphQL WebSockets")); - } + var requestDelegate = endpoints.CreateApplicationBuilder().UseMiddleware>().Build(); + return new GraphQLWebSocketsEndpointConventionBuilder(endpoints.Map(pattern, requestDelegate).WithDisplayName("GraphQL WebSockets")); } /// - /// Builds conventions that will be used for customization of Microsoft.AspNetCore.Builder.EndpointBuilder instances. - /// Special convention builder that allows you to write specific extension methods for ASP.NET Core routing subsystem. + /// Add the GraphQL web sockets middleware to the HTTP request pipeline /// - public class GraphQLWebSocketsEndpointConventionBuilder : IEndpointConventionBuilder + /// The implementation of to use + /// Custom middleware inherited from + /// Defines a contract for a route builder in an application. A route builder specifies the routes for an application. + /// The route pattern. + /// The received as parameter + public static GraphQLWebSocketsEndpointConventionBuilder MapGraphQLWebSockets(this IEndpointRouteBuilder endpoints, string pattern = "graphql") + where TSchema : ISchema + where TMiddleware : GraphQLWebSocketsMiddleware { - private readonly IEndpointConventionBuilder _builder; + if (endpoints == null) + throw new ArgumentNullException(nameof(endpoints)); - internal GraphQLWebSocketsEndpointConventionBuilder(IEndpointConventionBuilder builder) - { - _builder = builder; - } + var requestDelegate = endpoints.CreateApplicationBuilder().UseMiddleware().Build(); + return new GraphQLWebSocketsEndpointConventionBuilder(endpoints.Map(pattern, requestDelegate).WithDisplayName("GraphQL WebSockets")); + } +} - /// - public void Add(Action convention) => _builder.Add(convention); +/// +/// Builds conventions that will be used for customization of Microsoft.AspNetCore.Builder.EndpointBuilder instances. +/// Special convention builder that allows you to write specific extension methods for ASP.NET Core routing subsystem. +/// +public class GraphQLWebSocketsEndpointConventionBuilder : IEndpointConventionBuilder +{ + private readonly IEndpointConventionBuilder _builder; + + internal GraphQLWebSocketsEndpointConventionBuilder(IEndpointConventionBuilder builder) + { + _builder = builder; } + + /// + public void Add(Action convention) => _builder.Add(convention); } diff --git a/src/Transports.Subscriptions.WebSockets/GraphQLWebSocketsMiddleware.cs b/src/Transports.Subscriptions.WebSockets/GraphQLWebSocketsMiddleware.cs index 1978dd39..257be3f0 100644 --- a/src/Transports.Subscriptions.WebSockets/GraphQLWebSocketsMiddleware.cs +++ b/src/Transports.Subscriptions.WebSockets/GraphQLWebSocketsMiddleware.cs @@ -4,64 +4,63 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -namespace GraphQL.Server.Transports.WebSockets +namespace GraphQL.Server.Transports.WebSockets; + +/// +/// ASP.NET Core middleware for processing GraphQL web socket requests. This middleware useful with and without ASP.NET Core routing. +/// +/// Type of GraphQL schema that is used to process requests. +public class GraphQLWebSocketsMiddleware : IMiddleware + where TSchema : ISchema { - /// - /// ASP.NET Core middleware for processing GraphQL web socket requests. This middleware useful with and without ASP.NET Core routing. - /// - /// Type of GraphQL schema that is used to process requests. - public class GraphQLWebSocketsMiddleware : IMiddleware - where TSchema : ISchema + private readonly ILogger> _logger; + + public GraphQLWebSocketsMiddleware(ILogger> logger) { - private readonly ILogger> _logger; + _logger = logger; + } - public GraphQLWebSocketsMiddleware(ILogger> logger) + public virtual async Task InvokeAsync(HttpContext context, RequestDelegate next) + { + using (_logger.BeginScope(new Dictionary { - _logger = logger; - } - - public virtual async Task InvokeAsync(HttpContext context, RequestDelegate next) + ["ConnectionId"] = context.Connection.Id, + ["Request"] = context.Request + })) { - using (_logger.BeginScope(new Dictionary + if (!context.WebSockets.IsWebSocketRequest) { - ["ConnectionId"] = context.Connection.Id, - ["Request"] = context.Request - })) - { - if (!context.WebSockets.IsWebSocketRequest) - { - _logger.LogDebug("Request is not a valid websocket request"); - await next(context); + _logger.LogDebug("Request is not a valid websocket request"); + await next(context); - return; - } + return; + } - _logger.LogDebug("Connection is a valid websocket request"); + _logger.LogDebug("Connection is a valid websocket request"); - var socket = await context.WebSockets.AcceptWebSocketAsync("graphql-ws"); + var socket = await context.WebSockets.AcceptWebSocketAsync("graphql-ws"); - if (!context.WebSockets.WebSocketRequestedProtocols.Contains(socket.SubProtocol)) - { - _logger.LogError( - "Websocket connection does not have correct protocol: graphql-ws. Request protocols: {protocols}", - context.WebSockets.WebSocketRequestedProtocols); + if (!context.WebSockets.WebSocketRequestedProtocols.Contains(socket.SubProtocol)) + { + _logger.LogError( + "Websocket connection does not have correct protocol: graphql-ws. Request protocols: {protocols}", + context.WebSockets.WebSocketRequestedProtocols); - await socket.CloseAsync( - WebSocketCloseStatus.ProtocolError, - "Server only supports graphql-ws protocol", - context.RequestAborted); + await socket.CloseAsync( + WebSocketCloseStatus.ProtocolError, + "Server only supports graphql-ws protocol", + context.RequestAborted); - return; - } + return; + } - using (_logger.BeginScope($"GraphQL websocket connection: {context.Connection.Id}")) - { - var connectionFactory = context.RequestServices.GetRequiredService>(); - using var connection = connectionFactory.CreateConnection(socket, context.Connection.Id); + using (_logger.BeginScope($"GraphQL websocket connection: {context.Connection.Id}")) + { + var connectionFactory = context.RequestServices.GetRequiredService>(); + using var connection = connectionFactory.CreateConnection(socket, context.Connection.Id); - // Wait until the websocket has disconnected (and all subscriptions ended) - await connection.Connect(); - } + // Wait until the websocket has disconnected (and all subscriptions ended) + await connection.Connect(); } } } diff --git a/src/Transports.Subscriptions.WebSockets/IWebSocketConnectionFactory.cs b/src/Transports.Subscriptions.WebSockets/IWebSocketConnectionFactory.cs index b690cc19..d1c7401b 100644 --- a/src/Transports.Subscriptions.WebSockets/IWebSocketConnectionFactory.cs +++ b/src/Transports.Subscriptions.WebSockets/IWebSocketConnectionFactory.cs @@ -1,11 +1,10 @@ using System.Net.WebSockets; using GraphQL.Types; -namespace GraphQL.Server.Transports.WebSockets +namespace GraphQL.Server.Transports.WebSockets; + +public interface IWebSocketConnectionFactory + where TSchema : ISchema { - public interface IWebSocketConnectionFactory - where TSchema : ISchema - { - WebSocketConnection CreateConnection(WebSocket socket, string connectionId); - } + WebSocketConnection CreateConnection(WebSocket socket, string connectionId); } diff --git a/src/Transports.Subscriptions.WebSockets/WebSocketConnection.cs b/src/Transports.Subscriptions.WebSockets/WebSocketConnection.cs index f86e1133..9d9fc4d4 100644 --- a/src/Transports.Subscriptions.WebSockets/WebSocketConnection.cs +++ b/src/Transports.Subscriptions.WebSockets/WebSocketConnection.cs @@ -1,31 +1,30 @@ using GraphQL.Server.Transports.Subscriptions.Abstractions; -namespace GraphQL.Server.Transports.WebSockets +namespace GraphQL.Server.Transports.WebSockets; + +public class WebSocketConnection : IDisposable { - public class WebSocketConnection : IDisposable - { - private readonly WebSocketTransport _transport; - private readonly SubscriptionServer _server; + private readonly WebSocketTransport _transport; + private readonly SubscriptionServer _server; - public WebSocketConnection( - WebSocketTransport transport, - SubscriptionServer subscriptionServer) - { - _transport = transport; - _server = subscriptionServer; - } + public WebSocketConnection( + WebSocketTransport transport, + SubscriptionServer subscriptionServer) + { + _transport = transport; + _server = subscriptionServer; + } - public virtual async Task Connect() - { - await _server.OnConnect(); - await _server.OnDisconnect(); - await _transport.CloseAsync(); - } + public virtual async Task Connect() + { + await _server.OnConnect(); + await _server.OnDisconnect(); + await _transport.CloseAsync(); + } - public virtual void Dispose() - { - _server.Dispose(); - _transport.Dispose(); - } + public virtual void Dispose() + { + _server.Dispose(); + _transport.Dispose(); } } diff --git a/src/Transports.Subscriptions.WebSockets/WebSocketConnectionFactory.cs b/src/Transports.Subscriptions.WebSockets/WebSocketConnectionFactory.cs index ac785eaa..6d2aa7b7 100644 --- a/src/Transports.Subscriptions.WebSockets/WebSocketConnectionFactory.cs +++ b/src/Transports.Subscriptions.WebSockets/WebSocketConnectionFactory.cs @@ -4,48 +4,47 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -namespace GraphQL.Server.Transports.WebSockets +namespace GraphQL.Server.Transports.WebSockets; + +public class WebSocketConnectionFactory : IWebSocketConnectionFactory + where TSchema : ISchema { - public class WebSocketConnectionFactory : IWebSocketConnectionFactory - where TSchema : ISchema - { - private readonly ILogger _logger; - private readonly ILoggerFactory _loggerFactory; - private readonly IDocumentExecuter _executer; - private readonly IEnumerable _messageListeners; - private readonly IGraphQLTextSerializer _serializer; - private readonly IServiceScopeFactory _serviceScopeFactory; + private readonly ILogger _logger; + private readonly ILoggerFactory _loggerFactory; + private readonly IDocumentExecuter _executer; + private readonly IEnumerable _messageListeners; + private readonly IGraphQLTextSerializer _serializer; + private readonly IServiceScopeFactory _serviceScopeFactory; - public WebSocketConnectionFactory( - ILogger> logger, - ILoggerFactory loggerFactory, - IDocumentExecuter executer, - IEnumerable messageListeners, - IGraphQLTextSerializer serializer, - IServiceScopeFactory serviceScopeFactory) - { - _logger = logger; - _loggerFactory = loggerFactory; - _executer = executer; - _messageListeners = messageListeners; - _serviceScopeFactory = serviceScopeFactory; - _serializer = serializer; - } + public WebSocketConnectionFactory( + ILogger> logger, + ILoggerFactory loggerFactory, + IDocumentExecuter executer, + IEnumerable messageListeners, + IGraphQLTextSerializer serializer, + IServiceScopeFactory serviceScopeFactory) + { + _logger = logger; + _loggerFactory = loggerFactory; + _executer = executer; + _messageListeners = messageListeners; + _serviceScopeFactory = serviceScopeFactory; + _serializer = serializer; + } - public WebSocketConnection CreateConnection(WebSocket socket, string connectionId) - { - _logger.LogDebug("Creating server for connection {connectionId}", connectionId); + public WebSocketConnection CreateConnection(WebSocket socket, string connectionId) + { + _logger.LogDebug("Creating server for connection {connectionId}", connectionId); - var transport = new WebSocketTransport(socket, _serializer); - var manager = new SubscriptionManager(_executer, _loggerFactory, _serviceScopeFactory); - var server = new SubscriptionServer( - transport, - manager, - _messageListeners, - _loggerFactory.CreateLogger() - ); + var transport = new WebSocketTransport(socket, _serializer); + var manager = new SubscriptionManager(_executer, _loggerFactory, _serviceScopeFactory); + var server = new SubscriptionServer( + transport, + manager, + _messageListeners, + _loggerFactory.CreateLogger() + ); - return new WebSocketConnection(transport, server); - } + return new WebSocketConnection(transport, server); } } diff --git a/src/Transports.Subscriptions.WebSockets/WebSocketReaderPipeline.cs b/src/Transports.Subscriptions.WebSockets/WebSocketReaderPipeline.cs index 736e1481..10c4932b 100644 --- a/src/Transports.Subscriptions.WebSockets/WebSocketReaderPipeline.cs +++ b/src/Transports.Subscriptions.WebSockets/WebSocketReaderPipeline.cs @@ -4,155 +4,154 @@ using GraphQL.Server.Transports.Subscriptions.Abstractions; using GraphQL.Transport; -namespace GraphQL.Server.Transports.WebSockets +namespace GraphQL.Server.Transports.WebSockets; + +public class WebSocketReaderPipeline : IReaderPipeline { - public class WebSocketReaderPipeline : IReaderPipeline + private readonly IPropagatorBlock _endBlock; + private readonly IGraphQLTextSerializer _serializer; + private readonly WebSocket _socket; + private readonly ISourceBlock _startBlock; + + public WebSocketReaderPipeline(WebSocket socket, IGraphQLTextSerializer serializer) { - private readonly IPropagatorBlock _endBlock; - private readonly IGraphQLTextSerializer _serializer; - private readonly WebSocket _socket; - private readonly ISourceBlock _startBlock; + _socket = socket; + _serializer = serializer; - public WebSocketReaderPipeline(WebSocket socket, IGraphQLTextSerializer serializer) + _startBlock = CreateMessageReader(); + _endBlock = CreateReaderJsonTransformer(); + _startBlock.LinkTo(_endBlock, new DataflowLinkOptions { - _socket = socket; - _serializer = serializer; - - _startBlock = CreateMessageReader(); - _endBlock = CreateReaderJsonTransformer(); - _startBlock.LinkTo(_endBlock, new DataflowLinkOptions - { - PropagateCompletion = true - }); - } + PropagateCompletion = true + }); + } - public void LinkTo(ITargetBlock target) + public void LinkTo(ITargetBlock target) + { + _endBlock.LinkTo(target, new DataflowLinkOptions { - _endBlock.LinkTo(target, new DataflowLinkOptions - { - PropagateCompletion = true - }); - } + PropagateCompletion = true + }); + } - public Task Complete() => Complete(WebSocketCloseStatus.NormalClosure, "Completed"); + public Task Complete() => Complete(WebSocketCloseStatus.NormalClosure, "Completed"); - public async Task Complete(WebSocketCloseStatus closeStatus, string statusDescription) + public async Task Complete(WebSocketCloseStatus closeStatus, string statusDescription) + { + try { - try - { - if (_socket.State != WebSocketState.Closed && - _socket.State != WebSocketState.CloseSent && - _socket.State != WebSocketState.Aborted) - { - if (closeStatus == WebSocketCloseStatus.NormalClosure) - await _socket.CloseAsync( - closeStatus, - statusDescription, - CancellationToken.None); - else - await _socket.CloseOutputAsync( - closeStatus, - statusDescription, - CancellationToken.None); - } - } - finally + if (_socket.State != WebSocketState.Closed && + _socket.State != WebSocketState.CloseSent && + _socket.State != WebSocketState.Aborted) { - _startBlock.Complete(); + if (closeStatus == WebSocketCloseStatus.NormalClosure) + await _socket.CloseAsync( + closeStatus, + statusDescription, + CancellationToken.None); + else + await _socket.CloseOutputAsync( + closeStatus, + statusDescription, + CancellationToken.None); } } + finally + { + _startBlock.Complete(); + } + } - public Task Completion => _endBlock.Completion; + public Task Completion => _endBlock.Completion; - protected IPropagatorBlock CreateReaderJsonTransformer() - { - var transformer = new TransformBlock( - input => _serializer.Deserialize(input), - new ExecutionDataflowBlockOptions - { - EnsureOrdered = true - }); + protected IPropagatorBlock CreateReaderJsonTransformer() + { + var transformer = new TransformBlock( + input => _serializer.Deserialize(input), + new ExecutionDataflowBlockOptions + { + EnsureOrdered = true + }); - return transformer; - } + return transformer; + } - protected ISourceBlock CreateMessageReader() - { - IPropagatorBlock source = new BufferBlock( - new ExecutionDataflowBlockOptions - { - EnsureOrdered = true, - BoundedCapacity = 1, - MaxDegreeOfParallelism = 1 - }); + protected ISourceBlock CreateMessageReader() + { + IPropagatorBlock source = new BufferBlock( + new ExecutionDataflowBlockOptions + { + EnsureOrdered = true, + BoundedCapacity = 1, + MaxDegreeOfParallelism = 1 + }); - Task.Run(async () => await ReadMessageAsync(source)); + Task.Run(async () => await ReadMessageAsync(source)); - return source; - } + return source; + } - private async Task ReadMessageAsync(ITargetBlock target) + private async Task ReadMessageAsync(ITargetBlock target) + { + while (!_socket.CloseStatus.HasValue) { - while (!_socket.CloseStatus.HasValue) - { - string message; - byte[] buffer = new byte[1024 * 4]; - var segment = new ArraySegment(buffer); + string message; + byte[] buffer = new byte[1024 * 4]; + var segment = new ArraySegment(buffer); - using (var memoryStream = new MemoryStream()) + using (var memoryStream = new MemoryStream()) + { + try { - try + WebSocketReceiveResult receiveResult; + + do { - WebSocketReceiveResult receiveResult; + receiveResult = await _socket.ReceiveAsync(segment, CancellationToken.None); - do - { - receiveResult = await _socket.ReceiveAsync(segment, CancellationToken.None); + if (receiveResult.CloseStatus.HasValue) + target.Complete(); - if (receiveResult.CloseStatus.HasValue) - target.Complete(); + if (receiveResult.Count == 0) + continue; - if (receiveResult.Count == 0) - continue; + await memoryStream.WriteAsync(segment.Array, segment.Offset, receiveResult.Count); + } while (!receiveResult.EndOfMessage || memoryStream.Length == 0); - await memoryStream.WriteAsync(segment.Array, segment.Offset, receiveResult.Count); - } while (!receiveResult.EndOfMessage || memoryStream.Length == 0); + message = Encoding.UTF8.GetString(memoryStream.ToArray()); + } + catch (WebSocketException wx) + { + WebSocketCloseStatus closeStatus; - message = Encoding.UTF8.GetString(memoryStream.ToArray()); - } - catch (WebSocketException wx) - { - WebSocketCloseStatus closeStatus; - - switch (wx.WebSocketErrorCode) - { - case WebSocketError.ConnectionClosedPrematurely: - case WebSocketError.HeaderError: - case WebSocketError.UnsupportedProtocol: - case WebSocketError.UnsupportedVersion: - case WebSocketError.NotAWebSocket: - closeStatus = WebSocketCloseStatus.ProtocolError; - break; - case WebSocketError.InvalidMessageType: - closeStatus = WebSocketCloseStatus.InvalidMessageType; - break; - default: - closeStatus = WebSocketCloseStatus.InternalServerError; - break; - } - - await Complete(closeStatus, $"Closing socket connection due to {wx.WebSocketErrorCode}."); - break; - } - catch (Exception x) + switch (wx.WebSocketErrorCode) { - target.Fault(x); - continue; + case WebSocketError.ConnectionClosedPrematurely: + case WebSocketError.HeaderError: + case WebSocketError.UnsupportedProtocol: + case WebSocketError.UnsupportedVersion: + case WebSocketError.NotAWebSocket: + closeStatus = WebSocketCloseStatus.ProtocolError; + break; + case WebSocketError.InvalidMessageType: + closeStatus = WebSocketCloseStatus.InvalidMessageType; + break; + default: + closeStatus = WebSocketCloseStatus.InternalServerError; + break; } - } - await target.SendAsync(message); + await Complete(closeStatus, $"Closing socket connection due to {wx.WebSocketErrorCode}."); + break; + } + catch (Exception x) + { + target.Fault(x); + continue; + } } + + await target.SendAsync(message); } } } diff --git a/src/Transports.Subscriptions.WebSockets/WebSocketTransport.cs b/src/Transports.Subscriptions.WebSockets/WebSocketTransport.cs index 63870b11..cdbf8d77 100644 --- a/src/Transports.Subscriptions.WebSockets/WebSocketTransport.cs +++ b/src/Transports.Subscriptions.WebSockets/WebSocketTransport.cs @@ -1,46 +1,45 @@ using System.Net.WebSockets; using GraphQL.Server.Transports.Subscriptions.Abstractions; -namespace GraphQL.Server.Transports.WebSockets +namespace GraphQL.Server.Transports.WebSockets; + +public class WebSocketTransport : IMessageTransport, IDisposable { - public class WebSocketTransport : IMessageTransport, IDisposable - { - private readonly WebSocket _socket; + private readonly WebSocket _socket; - public WebSocketTransport(WebSocket socket, IGraphQLTextSerializer serializer) - { - _socket = socket; + public WebSocketTransport(WebSocket socket, IGraphQLTextSerializer serializer) + { + _socket = socket; - Reader = new WebSocketReaderPipeline(_socket, serializer); - Writer = new WebSocketWriterPipeline(_socket, serializer); - } + Reader = new WebSocketReaderPipeline(_socket, serializer); + Writer = new WebSocketWriterPipeline(_socket, serializer); + } - public WebSocketCloseStatus? CloseStatus => _socket.CloseStatus; + public WebSocketCloseStatus? CloseStatus => _socket.CloseStatus; - public IReaderPipeline Reader { get; } - public IWriterPipeline Writer { get; } + public IReaderPipeline Reader { get; } + public IWriterPipeline Writer { get; } - public Task CloseAsync() - { - if (_socket.State != WebSocketState.Open) - return Task.CompletedTask; + public Task CloseAsync() + { + if (_socket.State != WebSocketState.Open) + return Task.CompletedTask; - if (CloseStatus.HasValue) - if (CloseStatus != WebSocketCloseStatus.NormalClosure || CloseStatus != WebSocketCloseStatus.Empty) - return AbortAsync(); + if (CloseStatus.HasValue) + if (CloseStatus != WebSocketCloseStatus.NormalClosure || CloseStatus != WebSocketCloseStatus.Empty) + return AbortAsync(); - return _socket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closed", CancellationToken.None); - } + return _socket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closed", CancellationToken.None); + } - private Task AbortAsync() - { - _socket.Abort(); - return Task.CompletedTask; - } + private Task AbortAsync() + { + _socket.Abort(); + return Task.CompletedTask; + } - public void Dispose() - { - _socket.Dispose(); - } + public void Dispose() + { + _socket.Dispose(); } } diff --git a/src/Transports.Subscriptions.WebSockets/WebSocketWriterPipeline.cs b/src/Transports.Subscriptions.WebSockets/WebSocketWriterPipeline.cs index dc0d8292..55728ad7 100644 --- a/src/Transports.Subscriptions.WebSockets/WebSocketWriterPipeline.cs +++ b/src/Transports.Subscriptions.WebSockets/WebSocketWriterPipeline.cs @@ -3,61 +3,60 @@ using GraphQL.Server.Transports.Subscriptions.Abstractions; using GraphQL.Transport; -namespace GraphQL.Server.Transports.WebSockets +namespace GraphQL.Server.Transports.WebSockets; + +public class WebSocketWriterPipeline : IWriterPipeline { - public class WebSocketWriterPipeline : IWriterPipeline + private readonly WebSocket _socket; + private readonly IGraphQLSerializer _serializer; + private readonly ITargetBlock _startBlock; + + public WebSocketWriterPipeline(WebSocket socket, IGraphQLSerializer serializer) { - private readonly WebSocket _socket; - private readonly IGraphQLSerializer _serializer; - private readonly ITargetBlock _startBlock; + _socket = socket; + _serializer = serializer; - public WebSocketWriterPipeline(WebSocket socket, IGraphQLSerializer serializer) - { - _socket = socket; - _serializer = serializer; + _startBlock = CreateMessageWriter(); + } - _startBlock = CreateMessageWriter(); - } + public bool Post(OperationMessage message) => _startBlock.Post(message); - public bool Post(OperationMessage message) => _startBlock.Post(message); + public Task SendAsync(OperationMessage message) => _startBlock.SendAsync(message); - public Task SendAsync(OperationMessage message) => _startBlock.SendAsync(message); + public Task Completion => _startBlock.Completion; - public Task Completion => _startBlock.Completion; + public Task Complete() + { + _startBlock.Complete(); + return Task.CompletedTask; + } - public Task Complete() - { - _startBlock.Complete(); - return Task.CompletedTask; - } + private ITargetBlock CreateMessageWriter() + { + var target = new ActionBlock( + WriteMessageAsync, new ExecutionDataflowBlockOptions + { + MaxDegreeOfParallelism = 1, + EnsureOrdered = true + }); + + return target; + } + + private async Task WriteMessageAsync(OperationMessage message) + { + if (_socket.CloseStatus.HasValue) + return; - private ITargetBlock CreateMessageWriter() + var stream = new WebsocketWriterStream(_socket); + try { - var target = new ActionBlock( - WriteMessageAsync, new ExecutionDataflowBlockOptions - { - MaxDegreeOfParallelism = 1, - EnsureOrdered = true - }); - - return target; + await _serializer.WriteAsync(stream, message); } - - private async Task WriteMessageAsync(OperationMessage message) + finally { - if (_socket.CloseStatus.HasValue) - return; - - var stream = new WebsocketWriterStream(_socket); - try - { - await _serializer.WriteAsync(stream, message); - } - finally - { - await stream.FlushAsync(); - stream.Dispose(); - } + await stream.FlushAsync(); + stream.Dispose(); } } } diff --git a/src/Transports.Subscriptions.WebSockets/WebsocketWriterStream.cs b/src/Transports.Subscriptions.WebSockets/WebsocketWriterStream.cs index 59a8593f..86dcbcec 100644 --- a/src/Transports.Subscriptions.WebSockets/WebsocketWriterStream.cs +++ b/src/Transports.Subscriptions.WebSockets/WebsocketWriterStream.cs @@ -1,45 +1,44 @@ using System.Net.WebSockets; -namespace GraphQL.Server.Transports.WebSockets +namespace GraphQL.Server.Transports.WebSockets; + +public class WebsocketWriterStream : Stream { - public class WebsocketWriterStream : Stream - { - private readonly WebSocket _webSocket; + private readonly WebSocket _webSocket; - public WebsocketWriterStream(WebSocket webSocket) - { - _webSocket = webSocket; - } + public WebsocketWriterStream(WebSocket webSocket) + { + _webSocket = webSocket; + } - public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) - { - return _webSocket.SendAsync(new ArraySegment(buffer, offset, count), WebSocketMessageType.Text, false, - cancellationToken); - } + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + return _webSocket.SendAsync(new ArraySegment(buffer, offset, count), WebSocketMessageType.Text, false, + cancellationToken); + } - public override void Write(byte[] buffer, int offset, int count) => WriteAsync(buffer, offset, count).GetAwaiter().GetResult(); + public override void Write(byte[] buffer, int offset, int count) => WriteAsync(buffer, offset, count).GetAwaiter().GetResult(); - public override Task FlushAsync(CancellationToken cancellationToken) - => _webSocket.SendAsync(new ArraySegment(Array.Empty()), WebSocketMessageType.Text, true, cancellationToken); + public override Task FlushAsync(CancellationToken cancellationToken) + => _webSocket.SendAsync(new ArraySegment(Array.Empty()), WebSocketMessageType.Text, true, cancellationToken); - public override void Flush() => FlushAsync().GetAwaiter().GetResult(); + public override void Flush() => FlushAsync().GetAwaiter().GetResult(); - public override int Read(byte[] buffer, int offset, int count) => throw new NotSupportedException(); + public override int Read(byte[] buffer, int offset, int count) => throw new NotSupportedException(); - public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); + public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); - public override void SetLength(long value) => throw new NotSupportedException(); + public override void SetLength(long value) => throw new NotSupportedException(); - public override bool CanRead => false; - public override bool CanSeek => false; - public override bool CanWrite => true; + public override bool CanRead => false; + public override bool CanSeek => false; + public override bool CanWrite => true; - public override long Length => throw new NotSupportedException(); + public override long Length => throw new NotSupportedException(); - public override long Position - { - get => throw new NotSupportedException(); - set => throw new NotSupportedException(); - } + public override long Position + { + get => throw new NotSupportedException(); + set => throw new NotSupportedException(); } } diff --git a/src/Ui.Altair/AltairMiddleware.cs b/src/Ui.Altair/AltairMiddleware.cs index 00636b25..95f56f01 100644 --- a/src/Ui.Altair/AltairMiddleware.cs +++ b/src/Ui.Altair/AltairMiddleware.cs @@ -2,47 +2,46 @@ using GraphQL.Server.Ui.Altair.Internal; using Microsoft.AspNetCore.Http; -namespace GraphQL.Server.Ui.Altair +namespace GraphQL.Server.Ui.Altair; + +/// +/// A middleware for Altair GraphQL UI. +/// +public class AltairMiddleware { + private readonly AltairOptions _options; + /// - /// A middleware for Altair GraphQL UI. + /// The page model used to render Altair. /// - public class AltairMiddleware + private AltairPageModel? _pageModel; + + /// + /// Create a new + /// + /// The next request delegate; not used, this is a terminal middleware. + /// Options to customize middleware + [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "ASP.NET Core conventions")] + public AltairMiddleware(RequestDelegate next, AltairOptions options) { - private readonly AltairOptions _options; - - /// - /// The page model used to render Altair. - /// - private AltairPageModel? _pageModel; - - /// - /// Create a new - /// - /// The next request delegate; not used, this is a terminal middleware. - /// Options to customize middleware - [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "ASP.NET Core conventions")] - public AltairMiddleware(RequestDelegate next, AltairOptions options) - { - _options = options ?? throw new ArgumentNullException(nameof(options)); - } - - /// - /// Try to execute the logic of the middleware - /// - /// The HttpContext - public Task Invoke(HttpContext httpContext) - { - if (httpContext == null) - throw new ArgumentNullException(nameof(httpContext)); - - httpContext.Response.ContentType = "text/html"; - httpContext.Response.StatusCode = 200; - - _pageModel ??= new AltairPageModel(_options); - - byte[] data = Encoding.UTF8.GetBytes(_pageModel.Render()); - return httpContext.Response.Body.WriteAsync(data, 0, data.Length); - } + _options = options ?? throw new ArgumentNullException(nameof(options)); + } + + /// + /// Try to execute the logic of the middleware + /// + /// The HttpContext + public Task Invoke(HttpContext httpContext) + { + if (httpContext == null) + throw new ArgumentNullException(nameof(httpContext)); + + httpContext.Response.ContentType = "text/html"; + httpContext.Response.StatusCode = 200; + + _pageModel ??= new AltairPageModel(_options); + + byte[] data = Encoding.UTF8.GetBytes(_pageModel.Render()); + return httpContext.Response.Body.WriteAsync(data, 0, data.Length); } } diff --git a/src/Ui.Altair/AltairOptions.cs b/src/Ui.Altair/AltairOptions.cs index 17de9036..99cc934b 100644 --- a/src/Ui.Altair/AltairOptions.cs +++ b/src/Ui.Altair/AltairOptions.cs @@ -1,36 +1,35 @@ using Microsoft.AspNetCore.Http; -namespace GraphQL.Server.Ui.Altair +namespace GraphQL.Server.Ui.Altair; + +/// +/// Options to customize . +/// +public class AltairOptions { /// - /// Options to customize . + /// The GraphQL EndPoint. /// - public class AltairOptions - { - /// - /// The GraphQL EndPoint. - /// - public PathString GraphQLEndPoint { get; set; } = "/graphql"; + public PathString GraphQLEndPoint { get; set; } = "/graphql"; - /// - /// Subscriptions EndPoint. - /// - public PathString SubscriptionsEndPoint { get; set; } = "/graphql"; + /// + /// Subscriptions EndPoint. + /// + public PathString SubscriptionsEndPoint { get; set; } = "/graphql"; - /// - /// Altair headers configuration. - /// - public Dictionary? Headers { get; set; } + /// + /// Altair headers configuration. + /// + public Dictionary? Headers { get; set; } - /// - /// Gets or sets a Stream function for retrieving the Altair GraphQL UI page. - /// - public Func IndexStream { get; set; } = _ => typeof(AltairOptions).Assembly - .GetManifestResourceStream("GraphQL.Server.Ui.Altair.Internal.altair.cshtml")!; + /// + /// Gets or sets a Stream function for retrieving the Altair GraphQL UI page. + /// + public Func IndexStream { get; set; } = _ => typeof(AltairOptions).Assembly + .GetManifestResourceStream("GraphQL.Server.Ui.Altair.Internal.altair.cshtml")!; - /// - /// Gets or sets a delegate that is called after all transformations of the Altair GraphQL UI page. - /// - public Func PostConfigure { get; set; } = (options, result) => result; - } + /// + /// Gets or sets a delegate that is called after all transformations of the Altair GraphQL UI page. + /// + public Func PostConfigure { get; set; } = (options, result) => result; } diff --git a/src/Ui.Altair/Extensions/AltairApplicationBuilderExtensions.cs b/src/Ui.Altair/Extensions/AltairApplicationBuilderExtensions.cs index 9205babc..aabef772 100644 --- a/src/Ui.Altair/Extensions/AltairApplicationBuilderExtensions.cs +++ b/src/Ui.Altair/Extensions/AltairApplicationBuilderExtensions.cs @@ -1,29 +1,28 @@ using GraphQL.Server.Ui.Altair; -namespace Microsoft.AspNetCore.Builder +namespace Microsoft.AspNetCore.Builder; + +/// +/// Extensions for to add in the HTTP request pipeline. +/// +public static class AltairApplicationBuilderExtensions { - /// - /// Extensions for to add in the HTTP request pipeline. - /// - public static class AltairApplicationBuilderExtensions - { - /// Adds middleware for Altair GraphQL UI using default options. - /// to configure an application's request pipeline. - /// The path to the Altair GraphQL UI endpoint which defaults to '/ui/altair' - /// The reference to provided instance. - public static IApplicationBuilder UseGraphQLAltair(this IApplicationBuilder app, string path = "/ui/altair") - => app.UseGraphQLAltair(new AltairOptions(), path); + /// Adds middleware for Altair GraphQL UI using default options. + /// to configure an application's request pipeline. + /// The path to the Altair GraphQL UI endpoint which defaults to '/ui/altair' + /// The reference to provided instance. + public static IApplicationBuilder UseGraphQLAltair(this IApplicationBuilder app, string path = "/ui/altair") + => app.UseGraphQLAltair(new AltairOptions(), path); - /// Adds middleware for Altair GraphQL UI using the specified options. - /// to configure an application's request pipeline. - /// Options to customize . If not set, then the default values will be used. - /// The path to the Altair GraphQL UI endpoint which defaults to '/ui/altair' - /// The reference to provided instance. - public static IApplicationBuilder UseGraphQLAltair(this IApplicationBuilder app, AltairOptions options, string path = "/ui/altair") - { - return app.UseWhen( - context => context.Request.Path.StartsWithSegments(path, out var remaining) && string.IsNullOrEmpty(remaining), - b => b.UseMiddleware(options ?? new AltairOptions())); - } + /// Adds middleware for Altair GraphQL UI using the specified options. + /// to configure an application's request pipeline. + /// Options to customize . If not set, then the default values will be used. + /// The path to the Altair GraphQL UI endpoint which defaults to '/ui/altair' + /// The reference to provided instance. + public static IApplicationBuilder UseGraphQLAltair(this IApplicationBuilder app, AltairOptions options, string path = "/ui/altair") + { + return app.UseWhen( + context => context.Request.Path.StartsWithSegments(path, out var remaining) && string.IsNullOrEmpty(remaining), + b => b.UseMiddleware(options ?? new AltairOptions())); } } diff --git a/src/Ui.Altair/Extensions/AltairEndpointRouteBuilderExtensions.cs b/src/Ui.Altair/Extensions/AltairEndpointRouteBuilderExtensions.cs index f8c4e9d1..f2a59a25 100644 --- a/src/Ui.Altair/Extensions/AltairEndpointRouteBuilderExtensions.cs +++ b/src/Ui.Altair/Extensions/AltairEndpointRouteBuilderExtensions.cs @@ -1,53 +1,52 @@ using GraphQL.Server.Ui.Altair; using Microsoft.AspNetCore.Routing; -namespace Microsoft.AspNetCore.Builder +namespace Microsoft.AspNetCore.Builder; + +/// +/// Extensions for to add in the HTTP request pipeline. +/// +public static class AltairEndpointRouteBuilderExtensions { /// - /// Extensions for to add in the HTTP request pipeline. + /// Add the Altair middleware to the HTTP request pipeline /// - public static class AltairEndpointRouteBuilderExtensions - { - /// - /// Add the Altair middleware to the HTTP request pipeline - /// - /// Defines a contract for a route builder in an application. A route builder specifies the routes for an application. - /// The route pattern. - /// The received as parameter - public static AltairEndpointConventionBuilder MapGraphQLAltair(this IEndpointRouteBuilder endpoints, string pattern = "ui/altair") - => endpoints.MapGraphQLAltair(new AltairOptions(), pattern); - - /// - /// Add the Altair middleware to the HTTP request pipeline - /// - /// Defines a contract for a route builder in an application. A route builder specifies the routes for an application. - /// Options to customize . If not set, then the default values will be used. - /// The route pattern. - /// The received as parameter - public static AltairEndpointConventionBuilder MapGraphQLAltair(this IEndpointRouteBuilder endpoints, AltairOptions options, string pattern = "ui/altair") - { - if (endpoints == null) - throw new ArgumentNullException(nameof(endpoints)); - - var requestDelegate = endpoints.CreateApplicationBuilder().UseMiddleware(options ?? new AltairOptions()).Build(); - return new AltairEndpointConventionBuilder(endpoints.Map(pattern, requestDelegate).WithDisplayName("GraphQL Altair")); - } - } + /// Defines a contract for a route builder in an application. A route builder specifies the routes for an application. + /// The route pattern. + /// The received as parameter + public static AltairEndpointConventionBuilder MapGraphQLAltair(this IEndpointRouteBuilder endpoints, string pattern = "ui/altair") + => endpoints.MapGraphQLAltair(new AltairOptions(), pattern); /// - /// Builds conventions that will be used for customization of Microsoft.AspNetCore.Builder.EndpointBuilder instances. - /// Special convention builder that allows you to write specific extension methods for ASP.NET Core routing subsystem. + /// Add the Altair middleware to the HTTP request pipeline /// - public class AltairEndpointConventionBuilder : IEndpointConventionBuilder + /// Defines a contract for a route builder in an application. A route builder specifies the routes for an application. + /// Options to customize . If not set, then the default values will be used. + /// The route pattern. + /// The received as parameter + public static AltairEndpointConventionBuilder MapGraphQLAltair(this IEndpointRouteBuilder endpoints, AltairOptions options, string pattern = "ui/altair") { - private readonly IEndpointConventionBuilder _builder; + if (endpoints == null) + throw new ArgumentNullException(nameof(endpoints)); - internal AltairEndpointConventionBuilder(IEndpointConventionBuilder builder) - { - _builder = builder; - } + var requestDelegate = endpoints.CreateApplicationBuilder().UseMiddleware(options ?? new AltairOptions()).Build(); + return new AltairEndpointConventionBuilder(endpoints.Map(pattern, requestDelegate).WithDisplayName("GraphQL Altair")); + } +} + +/// +/// Builds conventions that will be used for customization of Microsoft.AspNetCore.Builder.EndpointBuilder instances. +/// Special convention builder that allows you to write specific extension methods for ASP.NET Core routing subsystem. +/// +public class AltairEndpointConventionBuilder : IEndpointConventionBuilder +{ + private readonly IEndpointConventionBuilder _builder; - /// - public void Add(Action convention) => _builder.Add(convention); + internal AltairEndpointConventionBuilder(IEndpointConventionBuilder builder) + { + _builder = builder; } + + /// + public void Add(Action convention) => _builder.Add(convention); } diff --git a/src/Ui.Altair/Internal/AltairPageModel.cs b/src/Ui.Altair/Internal/AltairPageModel.cs index b0bcb761..6ab8165c 100644 --- a/src/Ui.Altair/Internal/AltairPageModel.cs +++ b/src/Ui.Altair/Internal/AltairPageModel.cs @@ -1,48 +1,47 @@ using System.Text; using System.Text.Json; -namespace GraphQL.Server.Ui.Altair.Internal +namespace GraphQL.Server.Ui.Altair.Internal; + +// https://docs.microsoft.com/en-us/aspnet/core/mvc/razor-pages/?tabs=netcore-cli +internal sealed class AltairPageModel { - // https://docs.microsoft.com/en-us/aspnet/core/mvc/razor-pages/?tabs=netcore-cli - internal sealed class AltairPageModel - { - private string? _altairCSHtml; + private string? _altairCSHtml; - private readonly AltairOptions _options; + private readonly AltairOptions _options; - public AltairPageModel(AltairOptions options) - { - _options = options; - } + public AltairPageModel(AltairOptions options) + { + _options = options; + } - public string Render() + public string Render() + { + if (_altairCSHtml == null) { - if (_altairCSHtml == null) + using var manifestResourceStream = _options.IndexStream(_options); + using var streamReader = new StreamReader(manifestResourceStream); + + var headers = new Dictionary + { + ["Accept"] = "application/json", + ["Content-Type"] = "application/json", + }; + + if (_options.Headers?.Count > 0) { - using var manifestResourceStream = _options.IndexStream(_options); - using var streamReader = new StreamReader(manifestResourceStream); - - var headers = new Dictionary - { - ["Accept"] = "application/json", - ["Content-Type"] = "application/json", - }; - - if (_options.Headers?.Count > 0) - { - foreach (var item in _options.Headers) - headers[item.Key] = item.Value; - } - - var builder = new StringBuilder(streamReader.ReadToEnd()) - .Replace("@Model.GraphQLEndPoint", _options.GraphQLEndPoint) - .Replace("@Model.SubscriptionsEndPoint", _options.SubscriptionsEndPoint) - .Replace("@Model.Headers", JsonSerializer.Serialize(headers)); - - _altairCSHtml = _options.PostConfigure(_options, builder.ToString()); + foreach (var item in _options.Headers) + headers[item.Key] = item.Value; } - return _altairCSHtml; + var builder = new StringBuilder(streamReader.ReadToEnd()) + .Replace("@Model.GraphQLEndPoint", _options.GraphQLEndPoint) + .Replace("@Model.SubscriptionsEndPoint", _options.SubscriptionsEndPoint) + .Replace("@Model.Headers", JsonSerializer.Serialize(headers)); + + _altairCSHtml = _options.PostConfigure(_options, builder.ToString()); } + + return _altairCSHtml; } } diff --git a/src/Ui.GraphiQL/Extensions/GraphiQLApplicationBuilderExtensions.cs b/src/Ui.GraphiQL/Extensions/GraphiQLApplicationBuilderExtensions.cs index 117fa3ea..55e80dab 100644 --- a/src/Ui.GraphiQL/Extensions/GraphiQLApplicationBuilderExtensions.cs +++ b/src/Ui.GraphiQL/Extensions/GraphiQLApplicationBuilderExtensions.cs @@ -1,29 +1,28 @@ using GraphQL.Server.Ui.GraphiQL; -namespace Microsoft.AspNetCore.Builder +namespace Microsoft.AspNetCore.Builder; + +/// +/// Extensions for to add in the HTTP request pipeline. +/// +public static class GraphiQLApplicationBuilderExtensions { - /// - /// Extensions for to add in the HTTP request pipeline. - /// - public static class GraphiQLApplicationBuilderExtensions - { - /// Adds middleware for GraphiQL using default options. - /// to configure an application's request pipeline. - /// The path to the GraphiQL endpoint which defaults to '/ui/graphiql' - /// The reference to provided instance. - public static IApplicationBuilder UseGraphQLGraphiQL(this IApplicationBuilder app, string path = "/ui/graphiql") - => app.UseGraphQLGraphiQL(new GraphiQLOptions(), path); + /// Adds middleware for GraphiQL using default options. + /// to configure an application's request pipeline. + /// The path to the GraphiQL endpoint which defaults to '/ui/graphiql' + /// The reference to provided instance. + public static IApplicationBuilder UseGraphQLGraphiQL(this IApplicationBuilder app, string path = "/ui/graphiql") + => app.UseGraphQLGraphiQL(new GraphiQLOptions(), path); - /// Adds middleware for GraphiQL using the specified options. - /// to configure an application's request pipeline. - /// Options to customize . If not set, then the default values will be used. - /// The path to the GraphiQL endpoint which defaults to '/ui/graphiql' - /// The reference to provided instance. - public static IApplicationBuilder UseGraphQLGraphiQL(this IApplicationBuilder app, GraphiQLOptions options, string path = "/ui/graphiql") - { - return app.UseWhen( - context => context.Request.Path.StartsWithSegments(path, out var remaining) && string.IsNullOrEmpty(remaining), - b => b.UseMiddleware(options ?? new GraphiQLOptions())); - } + /// Adds middleware for GraphiQL using the specified options. + /// to configure an application's request pipeline. + /// Options to customize . If not set, then the default values will be used. + /// The path to the GraphiQL endpoint which defaults to '/ui/graphiql' + /// The reference to provided instance. + public static IApplicationBuilder UseGraphQLGraphiQL(this IApplicationBuilder app, GraphiQLOptions options, string path = "/ui/graphiql") + { + return app.UseWhen( + context => context.Request.Path.StartsWithSegments(path, out var remaining) && string.IsNullOrEmpty(remaining), + b => b.UseMiddleware(options ?? new GraphiQLOptions())); } } diff --git a/src/Ui.GraphiQL/Extensions/GraphiQLEndpointRouteBuilderExtensions.cs b/src/Ui.GraphiQL/Extensions/GraphiQLEndpointRouteBuilderExtensions.cs index 993d60bf..a5fb47ca 100644 --- a/src/Ui.GraphiQL/Extensions/GraphiQLEndpointRouteBuilderExtensions.cs +++ b/src/Ui.GraphiQL/Extensions/GraphiQLEndpointRouteBuilderExtensions.cs @@ -1,53 +1,52 @@ using GraphQL.Server.Ui.GraphiQL; using Microsoft.AspNetCore.Routing; -namespace Microsoft.AspNetCore.Builder +namespace Microsoft.AspNetCore.Builder; + +/// +/// Extensions for to add in the HTTP request pipeline. +/// +public static class GraphiQLEndpointRouteBuilderExtensions { /// - /// Extensions for to add in the HTTP request pipeline. + /// Add the GraphiQL middleware to the HTTP request pipeline /// - public static class GraphiQLEndpointRouteBuilderExtensions - { - /// - /// Add the GraphiQL middleware to the HTTP request pipeline - /// - /// Defines a contract for a route builder in an application. A route builder specifies the routes for an application. - /// The route pattern. - /// The received as parameter - public static GraphiQLEndpointConventionBuilder MapGraphQLGraphiQL(this IEndpointRouteBuilder endpoints, string pattern = "ui/graphiql") - => endpoints.MapGraphQLGraphiQL(new GraphiQLOptions(), pattern); - - /// - /// Add the GraphiQL middleware to the HTTP request pipeline - /// - /// Defines a contract for a route builder in an application. A route builder specifies the routes for an application. - /// Options to customize . If not set, then the default values will be used. - /// The route pattern. - /// The received as parameter - public static GraphiQLEndpointConventionBuilder MapGraphQLGraphiQL(this IEndpointRouteBuilder endpoints, GraphiQLOptions options, string pattern = "ui/graphiql") - { - if (endpoints == null) - throw new ArgumentNullException(nameof(endpoints)); - - var requestDelegate = endpoints.CreateApplicationBuilder().UseMiddleware(options ?? new GraphiQLOptions()).Build(); - return new GraphiQLEndpointConventionBuilder(endpoints.Map(pattern, requestDelegate).WithDisplayName("GraphiQL")); - } - } + /// Defines a contract for a route builder in an application. A route builder specifies the routes for an application. + /// The route pattern. + /// The received as parameter + public static GraphiQLEndpointConventionBuilder MapGraphQLGraphiQL(this IEndpointRouteBuilder endpoints, string pattern = "ui/graphiql") + => endpoints.MapGraphQLGraphiQL(new GraphiQLOptions(), pattern); /// - /// Builds conventions that will be used for customization of Microsoft.AspNetCore.Builder.EndpointBuilder instances. - /// Special convention builder that allows you to write specific extension methods for ASP.NET Core routing subsystem. + /// Add the GraphiQL middleware to the HTTP request pipeline /// - public class GraphiQLEndpointConventionBuilder : IEndpointConventionBuilder + /// Defines a contract for a route builder in an application. A route builder specifies the routes for an application. + /// Options to customize . If not set, then the default values will be used. + /// The route pattern. + /// The received as parameter + public static GraphiQLEndpointConventionBuilder MapGraphQLGraphiQL(this IEndpointRouteBuilder endpoints, GraphiQLOptions options, string pattern = "ui/graphiql") { - private readonly IEndpointConventionBuilder _builder; + if (endpoints == null) + throw new ArgumentNullException(nameof(endpoints)); - internal GraphiQLEndpointConventionBuilder(IEndpointConventionBuilder builder) - { - _builder = builder; - } + var requestDelegate = endpoints.CreateApplicationBuilder().UseMiddleware(options ?? new GraphiQLOptions()).Build(); + return new GraphiQLEndpointConventionBuilder(endpoints.Map(pattern, requestDelegate).WithDisplayName("GraphiQL")); + } +} + +/// +/// Builds conventions that will be used for customization of Microsoft.AspNetCore.Builder.EndpointBuilder instances. +/// Special convention builder that allows you to write specific extension methods for ASP.NET Core routing subsystem. +/// +public class GraphiQLEndpointConventionBuilder : IEndpointConventionBuilder +{ + private readonly IEndpointConventionBuilder _builder; - /// - public void Add(Action convention) => _builder.Add(convention); + internal GraphiQLEndpointConventionBuilder(IEndpointConventionBuilder builder) + { + _builder = builder; } + + /// + public void Add(Action convention) => _builder.Add(convention); } diff --git a/src/Ui.GraphiQL/GraphiQLMiddleware.cs b/src/Ui.GraphiQL/GraphiQLMiddleware.cs index 60c561e1..a67b8d5b 100644 --- a/src/Ui.GraphiQL/GraphiQLMiddleware.cs +++ b/src/Ui.GraphiQL/GraphiQLMiddleware.cs @@ -2,48 +2,47 @@ using GraphQL.Server.Ui.GraphiQL.Internal; using Microsoft.AspNetCore.Http; -namespace GraphQL.Server.Ui.GraphiQL +namespace GraphQL.Server.Ui.GraphiQL; + +/// +/// A middleware for GraphiQL UI. +/// +public class GraphiQLMiddleware { + private readonly GraphiQLOptions _options; + /// - /// A middleware for GraphiQL UI. + /// The page model used to render GraphiQL. /// - public class GraphiQLMiddleware + private GraphiQLPageModel? _pageModel; + + /// + /// Create a new + /// + /// The next request delegate; not used, this is a terminal middleware. + /// Options to customize middleware + [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "ASP.NET Core conventions")] + public GraphiQLMiddleware(RequestDelegate next, GraphiQLOptions options) { - private readonly GraphiQLOptions _options; - - /// - /// The page model used to render GraphiQL. - /// - private GraphiQLPageModel? _pageModel; - - /// - /// Create a new - /// - /// The next request delegate; not used, this is a terminal middleware. - /// Options to customize middleware - [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "ASP.NET Core conventions")] - public GraphiQLMiddleware(RequestDelegate next, GraphiQLOptions options) - { - _options = options ?? throw new ArgumentNullException(nameof(options)); - } - - /// - /// Try to execute the logic of the middleware - /// - /// The HttpContext - /// - public Task Invoke(HttpContext httpContext) - { - if (httpContext == null) - throw new ArgumentNullException(nameof(httpContext)); - - httpContext.Response.ContentType = "text/html"; - httpContext.Response.StatusCode = 200; - - _pageModel ??= new GraphiQLPageModel(_options); - - byte[] data = Encoding.UTF8.GetBytes(_pageModel.Render()); - return httpContext.Response.Body.WriteAsync(data, 0, data.Length); - } + _options = options ?? throw new ArgumentNullException(nameof(options)); + } + + /// + /// Try to execute the logic of the middleware + /// + /// The HttpContext + /// + public Task Invoke(HttpContext httpContext) + { + if (httpContext == null) + throw new ArgumentNullException(nameof(httpContext)); + + httpContext.Response.ContentType = "text/html"; + httpContext.Response.StatusCode = 200; + + _pageModel ??= new GraphiQLPageModel(_options); + + byte[] data = Encoding.UTF8.GetBytes(_pageModel.Render()); + return httpContext.Response.Body.WriteAsync(data, 0, data.Length); } } diff --git a/src/Ui.GraphiQL/GraphiQLOptions.cs b/src/Ui.GraphiQL/GraphiQLOptions.cs index 5a41fe3f..3a100004 100644 --- a/src/Ui.GraphiQL/GraphiQLOptions.cs +++ b/src/Ui.GraphiQL/GraphiQLOptions.cs @@ -1,50 +1,49 @@ using Microsoft.AspNetCore.Http; -namespace GraphQL.Server.Ui.GraphiQL +namespace GraphQL.Server.Ui.GraphiQL; + +/// +/// Options to customize the . +/// +public class GraphiQLOptions { /// - /// Options to customize the . + /// The GraphQL EndPoint. + /// + public PathString GraphQLEndPoint { get; set; } = "/graphql"; + + /// + /// Subscriptions EndPoint. + /// + public PathString SubscriptionsEndPoint { get; set; } = "/graphql"; + + /// + /// HTTP headers with which the GraphiQL will be initialized. + /// + public Dictionary? Headers { get; set; } + + /// + /// Gets or sets a Stream function for retrieving the GraphiQL UI page. + /// + public Func IndexStream { get; set; } = _ => typeof(GraphiQLOptions).Assembly + .GetManifestResourceStream("GraphQL.Server.Ui.GraphiQL.Internal.graphiql.cshtml")!; + + /// + /// Gets or sets a delegate that is called after all transformations of the GraphiQL UI page. + /// + public Func PostConfigure { get; set; } = (options, result) => result; + + /// + /// Enables the header editor when . + /// Not supported when is . + /// + /// + /// Original setting from GraphiQL. + /// + public bool HeaderEditorEnabled { get; set; } = true; + + /// + /// Enables the explorer extension when . /// - public class GraphiQLOptions - { - /// - /// The GraphQL EndPoint. - /// - public PathString GraphQLEndPoint { get; set; } = "/graphql"; - - /// - /// Subscriptions EndPoint. - /// - public PathString SubscriptionsEndPoint { get; set; } = "/graphql"; - - /// - /// HTTP headers with which the GraphiQL will be initialized. - /// - public Dictionary? Headers { get; set; } - - /// - /// Gets or sets a Stream function for retrieving the GraphiQL UI page. - /// - public Func IndexStream { get; set; } = _ => typeof(GraphiQLOptions).Assembly - .GetManifestResourceStream("GraphQL.Server.Ui.GraphiQL.Internal.graphiql.cshtml")!; - - /// - /// Gets or sets a delegate that is called after all transformations of the GraphiQL UI page. - /// - public Func PostConfigure { get; set; } = (options, result) => result; - - /// - /// Enables the header editor when . - /// Not supported when is . - /// - /// - /// Original setting from GraphiQL. - /// - public bool HeaderEditorEnabled { get; set; } = true; - - /// - /// Enables the explorer extension when . - /// - public bool ExplorerExtensionEnabled { get; set; } = true; - } + public bool ExplorerExtensionEnabled { get; set; } = true; } diff --git a/src/Ui.GraphiQL/Internal/GraphiQLPageModel.cs b/src/Ui.GraphiQL/Internal/GraphiQLPageModel.cs index 12731c16..42bc515f 100644 --- a/src/Ui.GraphiQL/Internal/GraphiQLPageModel.cs +++ b/src/Ui.GraphiQL/Internal/GraphiQLPageModel.cs @@ -1,50 +1,49 @@ using System.Text; using System.Text.Json; -namespace GraphQL.Server.Ui.GraphiQL.Internal +namespace GraphQL.Server.Ui.GraphiQL.Internal; + +// https://docs.microsoft.com/en-us/aspnet/core/mvc/razor-pages/?tabs=netcore-cli +internal sealed class GraphiQLPageModel { - // https://docs.microsoft.com/en-us/aspnet/core/mvc/razor-pages/?tabs=netcore-cli - internal sealed class GraphiQLPageModel - { - private string? _graphiQLCSHtml; + private string? _graphiQLCSHtml; - private readonly GraphiQLOptions _options; + private readonly GraphiQLOptions _options; - public GraphiQLPageModel(GraphiQLOptions options) - { - _options = options; - } + public GraphiQLPageModel(GraphiQLOptions options) + { + _options = options; + } - public string Render() + public string Render() + { + if (_graphiQLCSHtml == null) { - if (_graphiQLCSHtml == null) + using var manifestResourceStream = _options.IndexStream(_options); + using var streamReader = new StreamReader(manifestResourceStream); + + var headers = new Dictionary + { + ["Accept"] = "application/json", + ["Content-Type"] = "application/json", + }; + + if (_options.Headers?.Count > 0) { - using var manifestResourceStream = _options.IndexStream(_options); - using var streamReader = new StreamReader(manifestResourceStream); - - var headers = new Dictionary - { - ["Accept"] = "application/json", - ["Content-Type"] = "application/json", - }; - - if (_options.Headers?.Count > 0) - { - foreach (var item in _options.Headers) - headers[item.Key] = item.Value; - } - - var builder = new StringBuilder(streamReader.ReadToEnd()) - .Replace("@Model.GraphQLEndPoint", _options.GraphQLEndPoint) - .Replace("@Model.SubscriptionsEndPoint", _options.SubscriptionsEndPoint) - .Replace("@Model.Headers", JsonSerializer.Serialize(headers)) - .Replace("@Model.HeaderEditorEnabled", _options.HeaderEditorEnabled ? "true" : "false") - .Replace("@Model.GraphiQLElement", _options.ExplorerExtensionEnabled ? "GraphiQLWithExtensions.GraphiQLWithExtensions" : "GraphiQL"); - - _graphiQLCSHtml = _options.PostConfigure(_options, builder.ToString()); + foreach (var item in _options.Headers) + headers[item.Key] = item.Value; } - return _graphiQLCSHtml; + var builder = new StringBuilder(streamReader.ReadToEnd()) + .Replace("@Model.GraphQLEndPoint", _options.GraphQLEndPoint) + .Replace("@Model.SubscriptionsEndPoint", _options.SubscriptionsEndPoint) + .Replace("@Model.Headers", JsonSerializer.Serialize(headers)) + .Replace("@Model.HeaderEditorEnabled", _options.HeaderEditorEnabled ? "true" : "false") + .Replace("@Model.GraphiQLElement", _options.ExplorerExtensionEnabled ? "GraphiQLWithExtensions.GraphiQLWithExtensions" : "GraphiQL"); + + _graphiQLCSHtml = _options.PostConfigure(_options, builder.ToString()); } + + return _graphiQLCSHtml; } } diff --git a/src/Ui.Playground/Extensions/PlaygroundApplicationBuilderExtensions.cs b/src/Ui.Playground/Extensions/PlaygroundApplicationBuilderExtensions.cs index c72c00e6..227e8df0 100644 --- a/src/Ui.Playground/Extensions/PlaygroundApplicationBuilderExtensions.cs +++ b/src/Ui.Playground/Extensions/PlaygroundApplicationBuilderExtensions.cs @@ -1,29 +1,28 @@ using GraphQL.Server.Ui.Playground; -namespace Microsoft.AspNetCore.Builder +namespace Microsoft.AspNetCore.Builder; + +/// +/// Extensions for to add in the HTTP request pipeline. +/// +public static class PlaygroundApplicationBuilderExtensions { - /// - /// Extensions for to add in the HTTP request pipeline. - /// - public static class PlaygroundApplicationBuilderExtensions - { - /// Adds middleware for GraphQL Playground using default options. - /// to configure an application's request pipeline. - /// The path to the GraphQL Playground endpoint which defaults to '/ui/playground' - /// The reference to provided instance. - public static IApplicationBuilder UseGraphQLPlayground(this IApplicationBuilder app, string path = "/ui/playground") - => app.UseGraphQLPlayground(new PlaygroundOptions(), path); + /// Adds middleware for GraphQL Playground using default options. + /// to configure an application's request pipeline. + /// The path to the GraphQL Playground endpoint which defaults to '/ui/playground' + /// The reference to provided instance. + public static IApplicationBuilder UseGraphQLPlayground(this IApplicationBuilder app, string path = "/ui/playground") + => app.UseGraphQLPlayground(new PlaygroundOptions(), path); - /// Adds middleware for GraphQL Playground using the specified options. - /// to configure an application's request pipeline. - /// Options to customize . If not set, then the default values will be used. - /// The path to the GraphQL Playground endpoint which defaults to '/ui/playground' - /// The reference to provided instance. - public static IApplicationBuilder UseGraphQLPlayground(this IApplicationBuilder app, PlaygroundOptions options, string path = "/ui/playground") - { - return app.UseWhen( - context => context.Request.Path.StartsWithSegments(path, out var remaining) && string.IsNullOrEmpty(remaining), - b => b.UseMiddleware(options ?? new PlaygroundOptions())); - } + /// Adds middleware for GraphQL Playground using the specified options. + /// to configure an application's request pipeline. + /// Options to customize . If not set, then the default values will be used. + /// The path to the GraphQL Playground endpoint which defaults to '/ui/playground' + /// The reference to provided instance. + public static IApplicationBuilder UseGraphQLPlayground(this IApplicationBuilder app, PlaygroundOptions options, string path = "/ui/playground") + { + return app.UseWhen( + context => context.Request.Path.StartsWithSegments(path, out var remaining) && string.IsNullOrEmpty(remaining), + b => b.UseMiddleware(options ?? new PlaygroundOptions())); } } diff --git a/src/Ui.Playground/Extensions/PlaygroundEndpointRouteBuilderExtensions.cs b/src/Ui.Playground/Extensions/PlaygroundEndpointRouteBuilderExtensions.cs index 1997567a..965aad22 100644 --- a/src/Ui.Playground/Extensions/PlaygroundEndpointRouteBuilderExtensions.cs +++ b/src/Ui.Playground/Extensions/PlaygroundEndpointRouteBuilderExtensions.cs @@ -1,53 +1,52 @@ using GraphQL.Server.Ui.Playground; using Microsoft.AspNetCore.Routing; -namespace Microsoft.AspNetCore.Builder +namespace Microsoft.AspNetCore.Builder; + +/// +/// Extensions for to add in the HTTP request pipeline. +/// +public static class PlaygroundEndpointRouteBuilderExtensions { /// - /// Extensions for to add in the HTTP request pipeline. + /// Add the Playground middleware to the HTTP request pipeline /// - public static class PlaygroundEndpointRouteBuilderExtensions - { - /// - /// Add the Playground middleware to the HTTP request pipeline - /// - /// Defines a contract for a route builder in an application. A route builder specifies the routes for an application. - /// The route pattern. - /// The received as parameter - public static PlaygroundEndpointConventionBuilder MapGraphQLPlayground(this IEndpointRouteBuilder endpoints, string pattern = "ui/playground") - => endpoints.MapGraphQLPlayground(new PlaygroundOptions(), pattern); - - /// - /// Add the Playground middleware to the HTTP request pipeline - /// - /// Defines a contract for a route builder in an application. A route builder specifies the routes for an application. - /// Options to customize . If not set, then the default values will be used. - /// The route pattern. - /// The received as parameter - public static PlaygroundEndpointConventionBuilder MapGraphQLPlayground(this IEndpointRouteBuilder endpoints, PlaygroundOptions options, string pattern = "ui/playground") - { - if (endpoints == null) - throw new ArgumentNullException(nameof(endpoints)); - - var requestDelegate = endpoints.CreateApplicationBuilder().UseMiddleware(options ?? new PlaygroundOptions()).Build(); - return new PlaygroundEndpointConventionBuilder(endpoints.Map(pattern, requestDelegate).WithDisplayName("GraphQL Playground")); - } - } + /// Defines a contract for a route builder in an application. A route builder specifies the routes for an application. + /// The route pattern. + /// The received as parameter + public static PlaygroundEndpointConventionBuilder MapGraphQLPlayground(this IEndpointRouteBuilder endpoints, string pattern = "ui/playground") + => endpoints.MapGraphQLPlayground(new PlaygroundOptions(), pattern); /// - /// Builds conventions that will be used for customization of Microsoft.AspNetCore.Builder.EndpointBuilder instances. - /// Special convention builder that allows you to write specific extension methods for ASP.NET Core routing subsystem. + /// Add the Playground middleware to the HTTP request pipeline /// - public class PlaygroundEndpointConventionBuilder : IEndpointConventionBuilder + /// Defines a contract for a route builder in an application. A route builder specifies the routes for an application. + /// Options to customize . If not set, then the default values will be used. + /// The route pattern. + /// The received as parameter + public static PlaygroundEndpointConventionBuilder MapGraphQLPlayground(this IEndpointRouteBuilder endpoints, PlaygroundOptions options, string pattern = "ui/playground") { - private readonly IEndpointConventionBuilder _builder; + if (endpoints == null) + throw new ArgumentNullException(nameof(endpoints)); - internal PlaygroundEndpointConventionBuilder(IEndpointConventionBuilder builder) - { - _builder = builder; - } + var requestDelegate = endpoints.CreateApplicationBuilder().UseMiddleware(options ?? new PlaygroundOptions()).Build(); + return new PlaygroundEndpointConventionBuilder(endpoints.Map(pattern, requestDelegate).WithDisplayName("GraphQL Playground")); + } +} + +/// +/// Builds conventions that will be used for customization of Microsoft.AspNetCore.Builder.EndpointBuilder instances. +/// Special convention builder that allows you to write specific extension methods for ASP.NET Core routing subsystem. +/// +public class PlaygroundEndpointConventionBuilder : IEndpointConventionBuilder +{ + private readonly IEndpointConventionBuilder _builder; - /// - public void Add(Action convention) => _builder.Add(convention); + internal PlaygroundEndpointConventionBuilder(IEndpointConventionBuilder builder) + { + _builder = builder; } + + /// + public void Add(Action convention) => _builder.Add(convention); } diff --git a/src/Ui.Playground/Internal/PlaygroundPageModel.cs b/src/Ui.Playground/Internal/PlaygroundPageModel.cs index cb4efcd4..4fa0a4a6 100644 --- a/src/Ui.Playground/Internal/PlaygroundPageModel.cs +++ b/src/Ui.Playground/Internal/PlaygroundPageModel.cs @@ -1,59 +1,58 @@ using System.Text; using System.Text.Json; -namespace GraphQL.Server.Ui.Playground.Internal +namespace GraphQL.Server.Ui.Playground.Internal; + +// https://docs.microsoft.com/en-us/aspnet/core/mvc/razor-pages/?tabs=netcore-cli +internal sealed class PlaygroundPageModel { - // https://docs.microsoft.com/en-us/aspnet/core/mvc/razor-pages/?tabs=netcore-cli - internal sealed class PlaygroundPageModel - { - private string? _playgroundCSHtml; + private string? _playgroundCSHtml; - private readonly PlaygroundOptions _options; + private readonly PlaygroundOptions _options; - public PlaygroundPageModel(PlaygroundOptions options) - { - _options = options; - } + public PlaygroundPageModel(PlaygroundOptions options) + { + _options = options; + } - public string Render() + public string Render() + { + if (_playgroundCSHtml == null) { - if (_playgroundCSHtml == null) + using var manifestResourceStream = _options.IndexStream(_options); + using var streamReader = new StreamReader(manifestResourceStream); + + var headers = new Dictionary + { + ["Accept"] = "application/json", + // TODO: investigate, fails in Chrome + // { + // "error": "Response not successful: Received status code 400" + // } + // + // MediaTypeHeaderValue.TryParse(httpRequest.ContentType, out var mediaTypeHeader) from GraphQLHttpMiddleware + // returns false because of + // content-type: application/json, application/json + + //["Content-Type"] = "application/json", + }; + + if (_options.Headers?.Count > 0) { - using var manifestResourceStream = _options.IndexStream(_options); - using var streamReader = new StreamReader(manifestResourceStream); - - var headers = new Dictionary - { - ["Accept"] = "application/json", - // TODO: investigate, fails in Chrome - // { - // "error": "Response not successful: Received status code 400" - // } - // - // MediaTypeHeaderValue.TryParse(httpRequest.ContentType, out var mediaTypeHeader) from GraphQLHttpMiddleware - // returns false because of - // content-type: application/json, application/json - - //["Content-Type"] = "application/json", - }; - - if (_options.Headers?.Count > 0) - { - foreach (var item in _options.Headers) - headers[item.Key] = item.Value; - } - - var builder = new StringBuilder(streamReader.ReadToEnd()) - .Replace("@Model.GraphQLEndPoint", _options.GraphQLEndPoint) - .Replace("@Model.SubscriptionsEndPoint", _options.SubscriptionsEndPoint) - .Replace("@Model.GraphQLConfig", JsonSerializer.Serialize(_options.GraphQLConfig!)) - .Replace("@Model.Headers", JsonSerializer.Serialize(headers)) - .Replace("@Model.PlaygroundSettings", JsonSerializer.Serialize(_options.PlaygroundSettings)); - - _playgroundCSHtml = _options.PostConfigure(_options, builder.ToString()); + foreach (var item in _options.Headers) + headers[item.Key] = item.Value; } - return _playgroundCSHtml; + var builder = new StringBuilder(streamReader.ReadToEnd()) + .Replace("@Model.GraphQLEndPoint", _options.GraphQLEndPoint) + .Replace("@Model.SubscriptionsEndPoint", _options.SubscriptionsEndPoint) + .Replace("@Model.GraphQLConfig", JsonSerializer.Serialize(_options.GraphQLConfig!)) + .Replace("@Model.Headers", JsonSerializer.Serialize(headers)) + .Replace("@Model.PlaygroundSettings", JsonSerializer.Serialize(_options.PlaygroundSettings)); + + _playgroundCSHtml = _options.PostConfigure(_options, builder.ToString()); } + + return _playgroundCSHtml; } } diff --git a/src/Ui.Playground/PlaygroundMiddleware.cs b/src/Ui.Playground/PlaygroundMiddleware.cs index 0f8334fc..ab0b213e 100644 --- a/src/Ui.Playground/PlaygroundMiddleware.cs +++ b/src/Ui.Playground/PlaygroundMiddleware.cs @@ -2,47 +2,46 @@ using GraphQL.Server.Ui.Playground.Internal; using Microsoft.AspNetCore.Http; -namespace GraphQL.Server.Ui.Playground +namespace GraphQL.Server.Ui.Playground; + +/// +/// A middleware for GraphQL Playground UI. +/// +public class PlaygroundMiddleware { + private readonly PlaygroundOptions _options; + /// - /// A middleware for GraphQL Playground UI. + /// The page model used to render Playground. /// - public class PlaygroundMiddleware + private PlaygroundPageModel? _pageModel; + + /// + /// Create a new + /// + /// The next request delegate; not used, this is a terminal middleware. + /// Options to customize middleware + [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "ASP.NET Core conventions")] + public PlaygroundMiddleware(RequestDelegate next, PlaygroundOptions options) { - private readonly PlaygroundOptions _options; - - /// - /// The page model used to render Playground. - /// - private PlaygroundPageModel? _pageModel; - - /// - /// Create a new - /// - /// The next request delegate; not used, this is a terminal middleware. - /// Options to customize middleware - [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "ASP.NET Core conventions")] - public PlaygroundMiddleware(RequestDelegate next, PlaygroundOptions options) - { - _options = options ?? throw new ArgumentNullException(nameof(options)); - } - - /// - /// Try to execute the logic of the middleware - /// - /// The HttpContext - public Task Invoke(HttpContext httpContext) - { - if (httpContext == null) - throw new ArgumentNullException(nameof(httpContext)); - - httpContext.Response.ContentType = "text/html"; - httpContext.Response.StatusCode = 200; - - _pageModel ??= new PlaygroundPageModel(_options); - - byte[] data = Encoding.UTF8.GetBytes(_pageModel.Render()); - return httpContext.Response.Body.WriteAsync(data, 0, data.Length); - } + _options = options ?? throw new ArgumentNullException(nameof(options)); + } + + /// + /// Try to execute the logic of the middleware + /// + /// The HttpContext + public Task Invoke(HttpContext httpContext) + { + if (httpContext == null) + throw new ArgumentNullException(nameof(httpContext)); + + httpContext.Response.ContentType = "text/html"; + httpContext.Response.StatusCode = 200; + + _pageModel ??= new PlaygroundPageModel(_options); + + byte[] data = Encoding.UTF8.GetBytes(_pageModel.Render()); + return httpContext.Response.Body.WriteAsync(data, 0, data.Length); } } diff --git a/src/Ui.Playground/PlaygroundOptions.cs b/src/Ui.Playground/PlaygroundOptions.cs index c9680e15..1c646b87 100644 --- a/src/Ui.Playground/PlaygroundOptions.cs +++ b/src/Ui.Playground/PlaygroundOptions.cs @@ -1,185 +1,184 @@ using Microsoft.AspNetCore.Http; -namespace GraphQL.Server.Ui.Playground +namespace GraphQL.Server.Ui.Playground; + +/// +/// Options to customize . +/// +public class PlaygroundOptions { /// - /// Options to customize . + /// The GraphQL EndPoint. + /// + public PathString GraphQLEndPoint { get; set; } = "/graphql"; + + /// + /// Subscriptions EndPoint. + /// + public PathString SubscriptionsEndPoint { get; set; } = "/graphql"; + + /// + /// The GraphQL configuration. + /// + public Dictionary? GraphQLConfig { get; set; } + + /// + /// HTTP headers with which the GraphQL Playground will be initialized. /// - public class PlaygroundOptions + public Dictionary? Headers { get; set; } + + /// + /// Gets or sets a Stream function for retrieving the GraphQL Playground UI page. + /// + public Func IndexStream { get; set; } = _ => typeof(PlaygroundOptions).Assembly + .GetManifestResourceStream("GraphQL.Server.Ui.Playground.Internal.playground.cshtml")!; + + /// + /// Gets or sets a delegate that is called after all transformations of the GraphQL Playground UI page. + /// + public Func PostConfigure { get; set; } = (options, result) => result; + + /// + /// The GraphQL Playground Settings, see . + /// + public Dictionary PlaygroundSettings { get; set; } = new Dictionary(); + + /* typed settings below are just wrappers for PlaygroundSettings dictionary */ + + public EditorCursorShape EditorCursorShape { - /// - /// The GraphQL EndPoint. - /// - public PathString GraphQLEndPoint { get; set; } = "/graphql"; - - /// - /// Subscriptions EndPoint. - /// - public PathString SubscriptionsEndPoint { get; set; } = "/graphql"; - - /// - /// The GraphQL configuration. - /// - public Dictionary? GraphQLConfig { get; set; } - - /// - /// HTTP headers with which the GraphQL Playground will be initialized. - /// - public Dictionary? Headers { get; set; } - - /// - /// Gets or sets a Stream function for retrieving the GraphQL Playground UI page. - /// - public Func IndexStream { get; set; } = _ => typeof(PlaygroundOptions).Assembly - .GetManifestResourceStream("GraphQL.Server.Ui.Playground.Internal.playground.cshtml")!; - - /// - /// Gets or sets a delegate that is called after all transformations of the GraphQL Playground UI page. - /// - public Func PostConfigure { get; set; } = (options, result) => result; - - /// - /// The GraphQL Playground Settings, see . - /// - public Dictionary PlaygroundSettings { get; set; } = new Dictionary(); - - /* typed settings below are just wrappers for PlaygroundSettings dictionary */ - - public EditorCursorShape EditorCursorShape - { - get => (EditorCursorShape)Enum.Parse(typeof(EditorCursorShape), (string)PlaygroundSettings["editor.cursorShape"], ignoreCase: true); - set => PlaygroundSettings["editor.cursorShape"] = value.ToString().ToLower(); - } - - /// - /// Source Code Pro, Consolas, Inconsolata, Droid Sans Mono, Monaco, monospace. - /// - public string EditorFontFamily - { - get => (string)PlaygroundSettings["editor.fontFamily"]; - set => PlaygroundSettings["editor.fontFamily"] = value; - } + get => (EditorCursorShape)Enum.Parse(typeof(EditorCursorShape), (string)PlaygroundSettings["editor.cursorShape"], ignoreCase: true); + set => PlaygroundSettings["editor.cursorShape"] = value.ToString().ToLower(); + } - public int EditorFontSize - { - get => (int)PlaygroundSettings["editor.fontSize"]; - set => PlaygroundSettings["editor.fontSize"] = value; - } - - /// - /// New tab reuses headers from last tab. - /// - public bool EditorReuseHeaders - { - get => (bool)PlaygroundSettings["editor.reuseHeaders"]; - set => PlaygroundSettings["editor.reuseHeaders"] = value; - } + /// + /// Source Code Pro, Consolas, Inconsolata, Droid Sans Mono, Monaco, monospace. + /// + public string EditorFontFamily + { + get => (string)PlaygroundSettings["editor.fontFamily"]; + set => PlaygroundSettings["editor.fontFamily"] = value; + } - public EditorTheme EditorTheme - { - get => (EditorTheme)Enum.Parse(typeof(EditorTheme), (string)PlaygroundSettings["editor.theme"], ignoreCase: true); - set => PlaygroundSettings["editor.theme"] = value.ToString().ToLower(); - } + public int EditorFontSize + { + get => (int)PlaygroundSettings["editor.fontSize"]; + set => PlaygroundSettings["editor.fontSize"] = value; + } - public bool BetaUpdates - { - get => (bool)PlaygroundSettings["general.betaUpdates"]; - set => PlaygroundSettings["general.betaUpdates"] = value; - } + /// + /// New tab reuses headers from last tab. + /// + public bool EditorReuseHeaders + { + get => (bool)PlaygroundSettings["editor.reuseHeaders"]; + set => PlaygroundSettings["editor.reuseHeaders"] = value; + } - public int PrettierPrintWidth - { - get => (int)PlaygroundSettings["prettier.printWidth"]; - set => PlaygroundSettings["prettier.printWidth"] = value; - } + public EditorTheme EditorTheme + { + get => (EditorTheme)Enum.Parse(typeof(EditorTheme), (string)PlaygroundSettings["editor.theme"], ignoreCase: true); + set => PlaygroundSettings["editor.theme"] = value.ToString().ToLower(); + } - public int PrettierTabWidth - { - get => (int)PlaygroundSettings["prettier.tabWidth"]; - set => PlaygroundSettings["prettier.tabWidth"] = value; - } + public bool BetaUpdates + { + get => (bool)PlaygroundSettings["general.betaUpdates"]; + set => PlaygroundSettings["general.betaUpdates"] = value; + } - public bool PrettierUseTabs - { - get => (bool)PlaygroundSettings["prettier.useTabs"]; - set => PlaygroundSettings["prettier.useTabs"] = value; - } + public int PrettierPrintWidth + { + get => (int)PlaygroundSettings["prettier.printWidth"]; + set => PlaygroundSettings["prettier.printWidth"] = value; + } - public RequestCredentials RequestCredentials - { - get => (string)PlaygroundSettings["request.credentials"] switch - { - "omit" => RequestCredentials.Omit, - "include" => RequestCredentials.Include, - "same-origin" => RequestCredentials.SameOrigin, - _ => throw new NotSupportedException() - }; - set => PlaygroundSettings["request.credentials"] = value switch - { - RequestCredentials.Omit => "omit", - RequestCredentials.Include => "include", - RequestCredentials.SameOrigin => "same-origin", - _ => throw new NotSupportedException() - }; - } - - /// - /// Enables automatic schema polling. - /// - public bool SchemaPollingEnabled - { - get => (bool)PlaygroundSettings["schema.polling.enable"]; - set => PlaygroundSettings["schema.polling.enable"] = value; - } - - /// - /// Endpoint filter for schema polling, for example *localhost*. - /// - public string SchemaPollingEndpointFilter + public int PrettierTabWidth + { + get => (int)PlaygroundSettings["prettier.tabWidth"]; + set => PlaygroundSettings["prettier.tabWidth"] = value; + } + + public bool PrettierUseTabs + { + get => (bool)PlaygroundSettings["prettier.useTabs"]; + set => PlaygroundSettings["prettier.useTabs"] = value; + } + + public RequestCredentials RequestCredentials + { + get => (string)PlaygroundSettings["request.credentials"] switch { - get => (string)PlaygroundSettings["schema.polling.endpointFilter"]; - set => PlaygroundSettings["schema.polling.endpointFilter"] = value; - } - - /// - /// Schema polling interval in ms. - /// - public int SchemaPollingInterval + "omit" => RequestCredentials.Omit, + "include" => RequestCredentials.Include, + "same-origin" => RequestCredentials.SameOrigin, + _ => throw new NotSupportedException() + }; + set => PlaygroundSettings["request.credentials"] = value switch { - get => (int)PlaygroundSettings["schema.polling.interval"]; - set => PlaygroundSettings["schema.polling.interval"] = value; - } + RequestCredentials.Omit => "omit", + RequestCredentials.Include => "include", + RequestCredentials.SameOrigin => "same-origin", + _ => throw new NotSupportedException() + }; + } - public bool SchemaDisableComments - { - get => (bool)PlaygroundSettings["schema.disableComments"]; - set => PlaygroundSettings["schema.disableComments"] = value; - } + /// + /// Enables automatic schema polling. + /// + public bool SchemaPollingEnabled + { + get => (bool)PlaygroundSettings["schema.polling.enable"]; + set => PlaygroundSettings["schema.polling.enable"] = value; + } - public bool HideTracingResponse - { - get => (bool)PlaygroundSettings["tracing.hideTracingResponse"]; - set => PlaygroundSettings["tracing.hideTracingResponse"] = value; - } + /// + /// Endpoint filter for schema polling, for example *localhost*. + /// + public string SchemaPollingEndpointFilter + { + get => (string)PlaygroundSettings["schema.polling.endpointFilter"]; + set => PlaygroundSettings["schema.polling.endpointFilter"] = value; } - public enum EditorCursorShape + /// + /// Schema polling interval in ms. + /// + public int SchemaPollingInterval { - Line, - Block, - Underline + get => (int)PlaygroundSettings["schema.polling.interval"]; + set => PlaygroundSettings["schema.polling.interval"] = value; } - public enum EditorTheme + public bool SchemaDisableComments { - Dark, - Light + get => (bool)PlaygroundSettings["schema.disableComments"]; + set => PlaygroundSettings["schema.disableComments"] = value; } - public enum RequestCredentials + public bool HideTracingResponse { - Omit, - Include, - SameOrigin + get => (bool)PlaygroundSettings["tracing.hideTracingResponse"]; + set => PlaygroundSettings["tracing.hideTracingResponse"] = value; } } + +public enum EditorCursorShape +{ + Line, + Block, + Underline +} + +public enum EditorTheme +{ + Dark, + Light +} + +public enum RequestCredentials +{ + Omit, + Include, + SameOrigin +} diff --git a/src/Ui.Voyager/Extensions/VoyagerApplicationBuilderExtensions.cs b/src/Ui.Voyager/Extensions/VoyagerApplicationBuilderExtensions.cs index 0f72c44a..d19901b1 100644 --- a/src/Ui.Voyager/Extensions/VoyagerApplicationBuilderExtensions.cs +++ b/src/Ui.Voyager/Extensions/VoyagerApplicationBuilderExtensions.cs @@ -1,29 +1,28 @@ using GraphQL.Server.Ui.Voyager; -namespace Microsoft.AspNetCore.Builder +namespace Microsoft.AspNetCore.Builder; + +/// +/// Extensions for to add in the HTTP request pipeline. +/// +public static class VoyagerApplicationBuilderExtensions { - /// - /// Extensions for to add in the HTTP request pipeline. - /// - public static class VoyagerApplicationBuilderExtensions - { - /// Adds middleware for Voyager using default options. - /// to configure an application's request pipeline. - /// The path to the Voyager endpoint which defaults to '/ui/voyager' - /// The reference to provided instance. - public static IApplicationBuilder UseGraphQLVoyager(this IApplicationBuilder app, string path = "/ui/voyager") - => app.UseGraphQLVoyager(new VoyagerOptions(), path); + /// Adds middleware for Voyager using default options. + /// to configure an application's request pipeline. + /// The path to the Voyager endpoint which defaults to '/ui/voyager' + /// The reference to provided instance. + public static IApplicationBuilder UseGraphQLVoyager(this IApplicationBuilder app, string path = "/ui/voyager") + => app.UseGraphQLVoyager(new VoyagerOptions(), path); - /// Adds middleware for Voyager using the specified options. - /// to configure an application's request pipeline. - /// Options to customize . If not set, then the default values will be used. - /// The path to the Voyager endpoint which defaults to '/ui/voyager' - /// The reference to provided instance. - public static IApplicationBuilder UseGraphQLVoyager(this IApplicationBuilder app, VoyagerOptions options, string path = "/ui/voyager") - { - return app.UseWhen( - context => context.Request.Path.StartsWithSegments(path, out var remaining) && string.IsNullOrEmpty(remaining), - b => b.UseMiddleware(options ?? new VoyagerOptions())); - } + /// Adds middleware for Voyager using the specified options. + /// to configure an application's request pipeline. + /// Options to customize . If not set, then the default values will be used. + /// The path to the Voyager endpoint which defaults to '/ui/voyager' + /// The reference to provided instance. + public static IApplicationBuilder UseGraphQLVoyager(this IApplicationBuilder app, VoyagerOptions options, string path = "/ui/voyager") + { + return app.UseWhen( + context => context.Request.Path.StartsWithSegments(path, out var remaining) && string.IsNullOrEmpty(remaining), + b => b.UseMiddleware(options ?? new VoyagerOptions())); } } diff --git a/src/Ui.Voyager/Extensions/VoyagerEndpointRouteBuilderExtensions.cs b/src/Ui.Voyager/Extensions/VoyagerEndpointRouteBuilderExtensions.cs index 9a2ffd92..5f207947 100644 --- a/src/Ui.Voyager/Extensions/VoyagerEndpointRouteBuilderExtensions.cs +++ b/src/Ui.Voyager/Extensions/VoyagerEndpointRouteBuilderExtensions.cs @@ -1,53 +1,52 @@ using GraphQL.Server.Ui.Voyager; using Microsoft.AspNetCore.Routing; -namespace Microsoft.AspNetCore.Builder +namespace Microsoft.AspNetCore.Builder; + +/// +/// Extensions for to add in the HTTP request pipeline. +/// +public static class VoyagerEndpointRouteBuilderExtensions { /// - /// Extensions for to add in the HTTP request pipeline. + /// Add the Voyager middleware to the HTTP request pipeline /// - public static class VoyagerEndpointRouteBuilderExtensions - { - /// - /// Add the Voyager middleware to the HTTP request pipeline - /// - /// Defines a contract for a route builder in an application. A route builder specifies the routes for an application. - /// The route pattern. - /// The received as parameter - public static VoyagerEndpointConventionBuilder MapGraphQLVoyager(this IEndpointRouteBuilder endpoints, string pattern = "ui/voyager") - => endpoints.MapGraphQLVoyager(new VoyagerOptions(), pattern); - - /// - /// Add the Voyager middleware to the HTTP request pipeline - /// - /// Defines a contract for a route builder in an application. A route builder specifies the routes for an application. - /// Options to customize . If not set, then the default values will be used. - /// The route pattern. - /// The received as parameter - public static VoyagerEndpointConventionBuilder MapGraphQLVoyager(this IEndpointRouteBuilder endpoints, VoyagerOptions options, string pattern = "ui/voyager") - { - if (endpoints == null) - throw new ArgumentNullException(nameof(endpoints)); - - var requestDelegate = endpoints.CreateApplicationBuilder().UseMiddleware(options ?? new VoyagerOptions()).Build(); - return new VoyagerEndpointConventionBuilder(endpoints.Map(pattern, requestDelegate).WithDisplayName("GraphQL Voyager")); - } - } + /// Defines a contract for a route builder in an application. A route builder specifies the routes for an application. + /// The route pattern. + /// The received as parameter + public static VoyagerEndpointConventionBuilder MapGraphQLVoyager(this IEndpointRouteBuilder endpoints, string pattern = "ui/voyager") + => endpoints.MapGraphQLVoyager(new VoyagerOptions(), pattern); /// - /// Builds conventions that will be used for customization of Microsoft.AspNetCore.Builder.EndpointBuilder instances. - /// Special convention builder that allows you to write specific extension methods for ASP.NET Core routing subsystem. + /// Add the Voyager middleware to the HTTP request pipeline /// - public class VoyagerEndpointConventionBuilder : IEndpointConventionBuilder + /// Defines a contract for a route builder in an application. A route builder specifies the routes for an application. + /// Options to customize . If not set, then the default values will be used. + /// The route pattern. + /// The received as parameter + public static VoyagerEndpointConventionBuilder MapGraphQLVoyager(this IEndpointRouteBuilder endpoints, VoyagerOptions options, string pattern = "ui/voyager") { - private readonly IEndpointConventionBuilder _builder; + if (endpoints == null) + throw new ArgumentNullException(nameof(endpoints)); - internal VoyagerEndpointConventionBuilder(IEndpointConventionBuilder builder) - { - _builder = builder; - } + var requestDelegate = endpoints.CreateApplicationBuilder().UseMiddleware(options ?? new VoyagerOptions()).Build(); + return new VoyagerEndpointConventionBuilder(endpoints.Map(pattern, requestDelegate).WithDisplayName("GraphQL Voyager")); + } +} + +/// +/// Builds conventions that will be used for customization of Microsoft.AspNetCore.Builder.EndpointBuilder instances. +/// Special convention builder that allows you to write specific extension methods for ASP.NET Core routing subsystem. +/// +public class VoyagerEndpointConventionBuilder : IEndpointConventionBuilder +{ + private readonly IEndpointConventionBuilder _builder; - /// - public void Add(Action convention) => _builder.Add(convention); + internal VoyagerEndpointConventionBuilder(IEndpointConventionBuilder builder) + { + _builder = builder; } + + /// + public void Add(Action convention) => _builder.Add(convention); } diff --git a/src/Ui.Voyager/Internal/VoyagerPageModel.cs b/src/Ui.Voyager/Internal/VoyagerPageModel.cs index c79e2922..3485127d 100644 --- a/src/Ui.Voyager/Internal/VoyagerPageModel.cs +++ b/src/Ui.Voyager/Internal/VoyagerPageModel.cs @@ -1,47 +1,46 @@ using System.Text; using System.Text.Json; -namespace GraphQL.Server.Ui.Voyager.Internal +namespace GraphQL.Server.Ui.Voyager.Internal; + +// https://docs.microsoft.com/en-us/aspnet/core/mvc/razor-pages/?tabs=netcore-cli +internal sealed class VoyagerPageModel { - // https://docs.microsoft.com/en-us/aspnet/core/mvc/razor-pages/?tabs=netcore-cli - internal sealed class VoyagerPageModel - { - private string? _voyagerCSHtml; + private string? _voyagerCSHtml; - private readonly VoyagerOptions _options; + private readonly VoyagerOptions _options; - public VoyagerPageModel(VoyagerOptions options) - { - _options = options; - } + public VoyagerPageModel(VoyagerOptions options) + { + _options = options; + } - public string Render() + public string Render() + { + if (_voyagerCSHtml == null) { - if (_voyagerCSHtml == null) + using var manifestResourceStream = _options.IndexStream(_options); + using var streamReader = new StreamReader(manifestResourceStream); + + var headers = new Dictionary + { + ["Accept"] = "application/json", + ["Content-Type"] = "application/json", + }; + + if (_options.Headers?.Count > 0) { - using var manifestResourceStream = _options.IndexStream(_options); - using var streamReader = new StreamReader(manifestResourceStream); - - var headers = new Dictionary - { - ["Accept"] = "application/json", - ["Content-Type"] = "application/json", - }; - - if (_options.Headers?.Count > 0) - { - foreach (var item in _options.Headers) - headers[item.Key] = item.Value; - } - - var builder = new StringBuilder(streamReader.ReadToEnd()) - .Replace("@Model.GraphQLEndPoint", _options.GraphQLEndPoint) - .Replace("@Model.Headers", JsonSerializer.Serialize(headers)); - - _voyagerCSHtml = _options.PostConfigure(_options, builder.ToString()); + foreach (var item in _options.Headers) + headers[item.Key] = item.Value; } - return _voyagerCSHtml; + var builder = new StringBuilder(streamReader.ReadToEnd()) + .Replace("@Model.GraphQLEndPoint", _options.GraphQLEndPoint) + .Replace("@Model.Headers", JsonSerializer.Serialize(headers)); + + _voyagerCSHtml = _options.PostConfigure(_options, builder.ToString()); } + + return _voyagerCSHtml; } } diff --git a/src/Ui.Voyager/VoyagerMiddleware.cs b/src/Ui.Voyager/VoyagerMiddleware.cs index fd5e9455..af5b53f5 100644 --- a/src/Ui.Voyager/VoyagerMiddleware.cs +++ b/src/Ui.Voyager/VoyagerMiddleware.cs @@ -2,48 +2,47 @@ using GraphQL.Server.Ui.Voyager.Internal; using Microsoft.AspNetCore.Http; -namespace GraphQL.Server.Ui.Voyager +namespace GraphQL.Server.Ui.Voyager; + +/// +/// A middleware for Voyager UI. +/// +public class VoyagerMiddleware { + private readonly VoyagerOptions _options; + /// - /// A middleware for Voyager UI. + /// The page model used to render Voyager. /// - public class VoyagerMiddleware + private VoyagerPageModel? _pageModel; + + /// + /// Create a new + /// + /// The next request delegate; not used, this is a terminal middleware. + /// Options to customize middleware + [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "ASP.NET Core conventions")] + public VoyagerMiddleware(RequestDelegate next, VoyagerOptions options) { - private readonly VoyagerOptions _options; - - /// - /// The page model used to render Voyager. - /// - private VoyagerPageModel? _pageModel; - - /// - /// Create a new - /// - /// The next request delegate; not used, this is a terminal middleware. - /// Options to customize middleware - [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "ASP.NET Core conventions")] - public VoyagerMiddleware(RequestDelegate next, VoyagerOptions options) - { - _options = options ?? throw new ArgumentNullException(nameof(options)); - } - - /// - /// Try to execute the logic of the middleware - /// - /// The HttpContext - /// - public Task Invoke(HttpContext httpContext) - { - if (httpContext == null) - throw new ArgumentNullException(nameof(httpContext)); - - httpContext.Response.ContentType = "text/html"; - httpContext.Response.StatusCode = 200; - - _pageModel ??= new VoyagerPageModel(_options); - - byte[] data = Encoding.UTF8.GetBytes(_pageModel.Render()); - return httpContext.Response.Body.WriteAsync(data, 0, data.Length); - } + _options = options ?? throw new ArgumentNullException(nameof(options)); + } + + /// + /// Try to execute the logic of the middleware + /// + /// The HttpContext + /// + public Task Invoke(HttpContext httpContext) + { + if (httpContext == null) + throw new ArgumentNullException(nameof(httpContext)); + + httpContext.Response.ContentType = "text/html"; + httpContext.Response.StatusCode = 200; + + _pageModel ??= new VoyagerPageModel(_options); + + byte[] data = Encoding.UTF8.GetBytes(_pageModel.Render()); + return httpContext.Response.Body.WriteAsync(data, 0, data.Length); } } diff --git a/src/Ui.Voyager/VoyagerOptions.cs b/src/Ui.Voyager/VoyagerOptions.cs index d15797f2..b91ec472 100644 --- a/src/Ui.Voyager/VoyagerOptions.cs +++ b/src/Ui.Voyager/VoyagerOptions.cs @@ -1,31 +1,30 @@ using Microsoft.AspNetCore.Http; -namespace GraphQL.Server.Ui.Voyager +namespace GraphQL.Server.Ui.Voyager; + +/// +/// Options to customize . +/// +public class VoyagerOptions { /// - /// Options to customize . + /// The GraphQL EndPoint. /// - public class VoyagerOptions - { - /// - /// The GraphQL EndPoint. - /// - public PathString GraphQLEndPoint { get; set; } = "/graphql"; + public PathString GraphQLEndPoint { get; set; } = "/graphql"; - /// - /// HTTP headers with which the Voyager will send introspection query. - /// - public Dictionary? Headers { get; set; } + /// + /// HTTP headers with which the Voyager will send introspection query. + /// + public Dictionary? Headers { get; set; } - /// - /// Gets or sets a Stream function for retrieving the Voyager UI page. - /// - public Func IndexStream { get; set; } = _ => typeof(VoyagerOptions).Assembly - .GetManifestResourceStream("GraphQL.Server.Ui.Voyager.Internal.voyager.cshtml")!; + /// + /// Gets or sets a Stream function for retrieving the Voyager UI page. + /// + public Func IndexStream { get; set; } = _ => typeof(VoyagerOptions).Assembly + .GetManifestResourceStream("GraphQL.Server.Ui.Voyager.Internal.voyager.cshtml")!; - /// - /// Gets or sets a delegate that is called after all transformations of the Voyager UI page. - /// - public Func PostConfigure { get; set; } = (options, result) => result; - } + /// + /// Gets or sets a delegate that is called after all transformations of the Voyager UI page. + /// + public Func PostConfigure { get; set; } = (options, result) => result; } diff --git a/tests/ApiApprovalTests/ApiApprovalTests.cs b/tests/ApiApprovalTests/ApiApprovalTests.cs index 5d488665..b85f02b4 100644 --- a/tests/ApiApprovalTests/ApiApprovalTests.cs +++ b/tests/ApiApprovalTests/ApiApprovalTests.cs @@ -3,80 +3,79 @@ using System.Xml.Linq; using PublicApiGenerator; -namespace GraphQL.Authorization.ApiTests +namespace GraphQL.Authorization.ApiTests; + +/// +public class ApiApprovalTests { - /// - public class ApiApprovalTests + [Theory] + [InlineData(typeof(Server.Ui.Altair.AltairMiddleware))] + [InlineData(typeof(Server.Ui.GraphiQL.GraphiQLMiddleware))] + [InlineData(typeof(Server.Ui.Playground.PlaygroundMiddleware))] + [InlineData(typeof(Server.Ui.Voyager.VoyagerMiddleware))] + [InlineData(typeof(Server.Authorization.AspNetCore.AuthorizationValidationRule))] + [InlineData(typeof(Server.Transports.AspNetCore.GraphQLHttpMiddleware<>))] + [InlineData(typeof(Server.Transports.Subscriptions.Abstractions.SubscriptionServer))] + [InlineData(typeof(Server.Transports.WebSockets.WebSocketTransport))] + public void public_api_should_not_change_unintentionally(Type type) { - [Theory] - [InlineData(typeof(Server.Ui.Altair.AltairMiddleware))] - [InlineData(typeof(Server.Ui.GraphiQL.GraphiQLMiddleware))] - [InlineData(typeof(Server.Ui.Playground.PlaygroundMiddleware))] - [InlineData(typeof(Server.Ui.Voyager.VoyagerMiddleware))] - [InlineData(typeof(Server.Authorization.AspNetCore.AuthorizationValidationRule))] - [InlineData(typeof(Server.Transports.AspNetCore.GraphQLHttpMiddleware<>))] - [InlineData(typeof(Server.Transports.Subscriptions.Abstractions.SubscriptionServer))] - [InlineData(typeof(Server.Transports.WebSockets.WebSocketTransport))] - public void public_api_should_not_change_unintentionally(Type type) - { - string baseDir = AppDomain.CurrentDomain.BaseDirectory; - string projectName = type.Assembly.GetName().Name!; - string projectFolderName = projectName["GraphQL.Server.".Length..]; - string testDir = Path.Combine(baseDir, $"..{Path.DirectorySeparatorChar}..{Path.DirectorySeparatorChar}.."); - string projectDir = Path.Combine(testDir, $"..{Path.DirectorySeparatorChar}..", "src"); - string buildDir = Path.Combine(projectDir, projectFolderName, "bin", "Debug"); - Debug.Assert(Directory.Exists(buildDir), $"Directory '{buildDir}' doesn't exist"); - string csProject = Path.Combine(projectDir, projectFolderName, projectFolderName + ".csproj"); - var project = XDocument.Load(csProject); - string[] tfms = project.Descendants("TargetFrameworks").Union(project.Descendants("TargetFramework")).First().Value.Split(";", StringSplitOptions.RemoveEmptyEntries); + string baseDir = AppDomain.CurrentDomain.BaseDirectory; + string projectName = type.Assembly.GetName().Name!; + string projectFolderName = projectName["GraphQL.Server.".Length..]; + string testDir = Path.Combine(baseDir, $"..{Path.DirectorySeparatorChar}..{Path.DirectorySeparatorChar}.."); + string projectDir = Path.Combine(testDir, $"..{Path.DirectorySeparatorChar}..", "src"); + string buildDir = Path.Combine(projectDir, projectFolderName, "bin", "Debug"); + Debug.Assert(Directory.Exists(buildDir), $"Directory '{buildDir}' doesn't exist"); + string csProject = Path.Combine(projectDir, projectFolderName, projectFolderName + ".csproj"); + var project = XDocument.Load(csProject); + string[] tfms = project.Descendants("TargetFrameworks").Union(project.Descendants("TargetFramework")).First().Value.Split(";", StringSplitOptions.RemoveEmptyEntries); - // There may be old stuff from earlier builds like net45, netcoreapp3.0, etc. so filter it out - string[] actualTfmDirs = Directory.GetDirectories(buildDir).Where(dir => tfms.Any(tfm => dir.EndsWith(tfm))).ToArray(); - Debug.Assert(actualTfmDirs.Length > 0, $"Directory '{buildDir}' doesn't contain subdirectories matching {string.Join(";", tfms)}"); + // There may be old stuff from earlier builds like net45, netcoreapp3.0, etc. so filter it out + string[] actualTfmDirs = Directory.GetDirectories(buildDir).Where(dir => tfms.Any(tfm => dir.EndsWith(tfm))).ToArray(); + Debug.Assert(actualTfmDirs.Length > 0, $"Directory '{buildDir}' doesn't contain subdirectories matching {string.Join(";", tfms)}"); - (string tfm, string content)[] publicApi = actualTfmDirs.Select(tfmDir => (new DirectoryInfo(tfmDir).Name.Replace(".", ""), Assembly.LoadFile(Path.Combine(tfmDir, projectName + ".dll")).GeneratePublicApi(new ApiGeneratorOptions - { - IncludeAssemblyAttributes = false, - WhitelistedNamespacePrefixes = new[] { "Microsoft." }, - ExcludeAttributes = new[] { "System.Diagnostics.DebuggerDisplayAttribute", "System.Diagnostics.CodeAnalysis.AllowNullAttribute" } - }))).ToArray(); + (string tfm, string content)[] publicApi = actualTfmDirs.Select(tfmDir => (new DirectoryInfo(tfmDir).Name.Replace(".", ""), Assembly.LoadFile(Path.Combine(tfmDir, projectName + ".dll")).GeneratePublicApi(new ApiGeneratorOptions + { + IncludeAssemblyAttributes = false, + WhitelistedNamespacePrefixes = new[] { "Microsoft." }, + ExcludeAttributes = new[] { "System.Diagnostics.DebuggerDisplayAttribute", "System.Diagnostics.CodeAnalysis.AllowNullAttribute" } + }))).ToArray(); - if (publicApi.DistinctBy(item => item.content).Count() == 1) + if (publicApi.DistinctBy(item => item.content).Count() == 1) + { + AutoApproveOrFail(publicApi[0].content, ""); + } + else + { + foreach (var item in publicApi.ToLookup(item => item.content)) { - AutoApproveOrFail(publicApi[0].content, ""); + AutoApproveOrFail(item.Key, string.Join("+", item.Select(x => x.tfm).OrderBy(x => x))); } - else + } + + // Approval test should (re)generate approved.txt files locally if needed. + // Approval test should fail on CI. + // https://docs.github.com/en/actions/learn-github-actions/environment-variables#default-environment-variables + void AutoApproveOrFail(string publicApi, string folder) + { + string file = null!; + + try { - foreach (var item in publicApi.ToLookup(item => item.content)) - { - AutoApproveOrFail(item.Key, string.Join("+", item.Select(x => x.tfm).OrderBy(x => x))); - } + publicApi.ShouldMatchApproved(options => options.SubFolder(folder).NoDiff().WithFilenameGenerator((testMethodInfo, discriminator, fileType, fileExtension) => file = $"{type.Assembly.GetName().Name}.{fileType}.{fileExtension}")); } - - // Approval test should (re)generate approved.txt files locally if needed. - // Approval test should fail on CI. - // https://docs.github.com/en/actions/learn-github-actions/environment-variables#default-environment-variables - void AutoApproveOrFail(string publicApi, string folder) + catch (ShouldMatchApprovedException) when (Environment.GetEnvironmentVariable("CI") == null) { - string file = null!; - - try + string? received = Path.Combine(testDir, folder, file); + string? approved = received.Replace(".received.txt", ".approved.txt"); + if (File.Exists(received) && File.Exists(approved)) { - publicApi.ShouldMatchApproved(options => options.SubFolder(folder).NoDiff().WithFilenameGenerator((testMethodInfo, discriminator, fileType, fileExtension) => file = $"{type.Assembly.GetName().Name}.{fileType}.{fileExtension}")); + File.Copy(received, approved, overwrite: true); + File.Delete(received); } - catch (ShouldMatchApprovedException) when (Environment.GetEnvironmentVariable("CI") == null) + else { - string? received = Path.Combine(testDir, folder, file); - string? approved = received.Replace(".received.txt", ".approved.txt"); - if (File.Exists(received) && File.Exists(approved)) - { - File.Copy(received, approved, overwrite: true); - File.Delete(received); - } - else - { - throw; - } + throw; } } } diff --git a/tests/Authorization.AspNetCore.Tests/AuthorizationValidationRuleTests.cs b/tests/Authorization.AspNetCore.Tests/AuthorizationValidationRuleTests.cs index c03b0bd0..a152cda1 100644 --- a/tests/Authorization.AspNetCore.Tests/AuthorizationValidationRuleTests.cs +++ b/tests/Authorization.AspNetCore.Tests/AuthorizationValidationRuleTests.cs @@ -1,337 +1,337 @@ using GraphQL.Types; using GraphQL.Types.Relay.DataObjects; -namespace GraphQL.Server.Authorization.AspNetCore.Tests +namespace GraphQL.Server.Authorization.AspNetCore.Tests; + +public class AuthorizationValidationRuleTests : ValidationTestBase { - public class AuthorizationValidationRuleTests : ValidationTestBase + // https://github.com/graphql-dotnet/server/issues/463 + [Fact] + public void policy_on_schema_success() { - // https://github.com/graphql-dotnet/server/issues/463 - [Fact] - public void policy_on_schema_success() + ConfigureAuthorizationOptions(options => { - ConfigureAuthorizationOptions(options => - { - options.AddPolicy("ClassPolicy", x => x.RequireClaim("admin")); - options.AddPolicy("SchemaPolicy", x => x.RequireClaim("some")); - }); + options.AddPolicy("ClassPolicy", x => x.RequireClaim("admin")); + options.AddPolicy("SchemaPolicy", x => x.RequireClaim("some")); + }); - ShouldPassRule(config => + ShouldPassRule(config => + { + config.Query = @"query { post }"; + config.Schema = BasicSchema().AuthorizeWithPolicy("SchemaPolicy"); + config.User = CreatePrincipal(claims: new Dictionary { - config.Query = @"query { post }"; - config.Schema = BasicSchema().AuthorizeWithPolicy("SchemaPolicy"); - config.User = CreatePrincipal(claims: new Dictionary - { - { "Admin", "true" }, - { "some", "abcdef" } - }); + { "Admin", "true" }, + { "some", "abcdef" } }); - } + }); + } + + // https://github.com/graphql-dotnet/server/issues/463 + [Fact] + public void policy_on_schema_fail() + { + ConfigureAuthorizationOptions(options => + { + options.AddPolicy("ClassPolicy", x => x.RequireClaim("admin")); + options.AddPolicy("SchemaPolicy", x => x.RequireClaim("some")); + }); - // https://github.com/graphql-dotnet/server/issues/463 - [Fact] - public void policy_on_schema_fail() + ShouldFailRule(config => { - ConfigureAuthorizationOptions(options => + config.Query = @"query { post }"; + config.Schema = BasicSchema().AuthorizeWithPolicy("SchemaPolicy"); + config.User = CreatePrincipal(claims: new Dictionary { - options.AddPolicy("ClassPolicy", x => x.RequireClaim("admin")); - options.AddPolicy("SchemaPolicy", x => x.RequireClaim("some")); + { "Admin", "true" }, }); - - ShouldFailRule(config => + config.ValidateResult = result => { - config.Query = @"query { post }"; - config.Schema = BasicSchema().AuthorizeWithPolicy("SchemaPolicy"); - config.User = CreatePrincipal(claims: new Dictionary - { - { "Admin", "true" }, - }); - config.ValidateResult = result => - { - result.Errors.Count.ShouldBe(1); - result.Errors[0].Message.ShouldBe(@"You are not authorized to run this operation. + result.Errors.Count.ShouldBe(1); + result.Errors[0].Message.ShouldBe(@"You are not authorized to run this operation. Required claim 'some' is not present."); - }; - }); - } + }; + }); + } - [Fact] - public void class_policy_success() - { - ConfigureAuthorizationOptions(options => options.AddPolicy("ClassPolicy", x => x.RequireClaim("admin"))); + [Fact] + public void class_policy_success() + { + ConfigureAuthorizationOptions(options => options.AddPolicy("ClassPolicy", x => x.RequireClaim("admin"))); - ShouldPassRule(config => + ShouldPassRule(config => + { + config.Query = @"query { post }"; + config.Schema = BasicSchema(); + config.User = CreatePrincipal(claims: new Dictionary { - config.Query = @"query { post }"; - config.Schema = BasicSchema(); - config.User = CreatePrincipal(claims: new Dictionary - { - { "Admin", "true" } - }); + { "Admin", "true" } }); - } + }); + } - [Fact] - public void class_policy_fail() - { - ConfigureAuthorizationOptions(options => options.AddPolicy("ClassPolicy", x => x.RequireClaim("admin"))); + [Fact] + public void class_policy_fail() + { + ConfigureAuthorizationOptions(options => options.AddPolicy("ClassPolicy", x => x.RequireClaim("admin"))); - ShouldFailRule(config => + ShouldFailRule(config => + { + config.Query = @"query { post }"; + config.Schema = BasicSchema(); + config.ValidateResult = result => { - config.Query = @"query { post }"; - config.Schema = BasicSchema(); - config.ValidateResult = result => - { - result.Errors.Count.ShouldBe(1); - result.Errors[0].Message.ShouldBe(@"You are not authorized to run this query. + result.Errors.Count.ShouldBe(1); + result.Errors[0].Message.ShouldBe(@"You are not authorized to run this query. Required claim 'admin' is not present."); - }; - }); - } + }; + }); + } - [Fact] - public void method_policy_success() - { - ConfigureAuthorizationOptions(options => options.AddPolicy("FieldPolicy", x => x.RequireClaim("admin"))); + [Fact] + public void method_policy_success() + { + ConfigureAuthorizationOptions(options => options.AddPolicy("FieldPolicy", x => x.RequireClaim("admin"))); - ShouldPassRule(config => + ShouldPassRule(config => + { + config.Query = @"query { post }"; + config.Schema = BasicSchema(); + config.User = CreatePrincipal(claims: new Dictionary { - config.Query = @"query { post }"; - config.Schema = BasicSchema(); - config.User = CreatePrincipal(claims: new Dictionary - { - { "Admin", "true" } - }); + { "Admin", "true" } }); - } + }); + } - [Fact] - public void property_policy_success() - { - ConfigureAuthorizationOptions(options => options.AddPolicy("FieldPolicy", x => x.RequireClaim("admin"))); + [Fact] + public void property_policy_success() + { + ConfigureAuthorizationOptions(options => options.AddPolicy("FieldPolicy", x => x.RequireClaim("admin"))); - ShouldPassRule(config => + ShouldPassRule(config => + { + config.Query = @"query { post }"; + config.Schema = BasicSchema(); + config.User = CreatePrincipal(claims: new Dictionary { - config.Query = @"query { post }"; - config.Schema = BasicSchema(); - config.User = CreatePrincipal(claims: new Dictionary - { - { "Admin", "true" } - }); + { "Admin", "true" } }); - } + }); + } - [Fact] - public void method_policy_fail() - { - ConfigureAuthorizationOptions(options => options.AddPolicy("FieldPolicy", x => x.RequireClaim("admin"))); + [Fact] + public void method_policy_fail() + { + ConfigureAuthorizationOptions(options => options.AddPolicy("FieldPolicy", x => x.RequireClaim("admin"))); - ShouldFailRule(config => + ShouldFailRule(config => + { + config.Query = @"query { post }"; + config.Schema = BasicSchema(); + config.ValidateResult = result => { - config.Query = @"query { post }"; - config.Schema = BasicSchema(); - config.ValidateResult = result => - { - result.Errors.Count.ShouldBe(1); - result.Errors[0].Message.ShouldBe(@"You are not authorized to run this query. + result.Errors.Count.ShouldBe(1); + result.Errors[0].Message.ShouldBe(@"You are not authorized to run this query. Required claim 'admin' is not present."); - }; - }); - } + }; + }); + } - [Fact] - public void property_policy_fail() - { - ConfigureAuthorizationOptions(options => options.AddPolicy("FieldPolicy", x => x.RequireClaim("admin"))); + [Fact] + public void property_policy_fail() + { + ConfigureAuthorizationOptions(options => options.AddPolicy("FieldPolicy", x => x.RequireClaim("admin"))); - ShouldFailRule(config => + ShouldFailRule(config => + { + config.Query = @"query { post }"; + config.Schema = BasicSchema(); + config.ValidateResult = result => { - config.Query = @"query { post }"; - config.Schema = BasicSchema(); - config.ValidateResult = result => - { - result.Errors.Count.ShouldBe(1); - result.Errors[0].Message.ShouldBe(@"You are not authorized to run this query. + result.Errors.Count.ShouldBe(1); + result.Errors[0].Message.ShouldBe(@"You are not authorized to run this query. Required claim 'admin' is not present."); - }; - }); - } + }; + }); + } - [Fact] - public void nested_type_policy_success() - { - ConfigureAuthorizationOptions(options => options.AddPolicy("PostPolicy", x => x.RequireClaim("admin"))); + [Fact] + public void nested_type_policy_success() + { + ConfigureAuthorizationOptions(options => options.AddPolicy("PostPolicy", x => x.RequireClaim("admin"))); - ShouldPassRule(config => + ShouldPassRule(config => + { + config.Query = @"query { post }"; + config.Schema = NestedSchema(); + config.User = CreatePrincipal(claims: new Dictionary { - config.Query = @"query { post }"; - config.Schema = NestedSchema(); - config.User = CreatePrincipal(claims: new Dictionary - { - { "Admin", "true" } - }); + { "Admin", "true" } }); - } + }); + } - [Fact] - public void nested_type_policy_fail() - { - ConfigureAuthorizationOptions(options => options.AddPolicy("PostPolicy", x => x.RequireClaim("admin"))); + [Fact] + public void nested_type_policy_fail() + { + ConfigureAuthorizationOptions(options => options.AddPolicy("PostPolicy", x => x.RequireClaim("admin"))); - ShouldFailRule(config => + ShouldFailRule(config => + { + config.Query = @"query { post }"; + config.Schema = NestedSchema(); + config.ValidateResult = result => { - config.Query = @"query { post }"; - config.Schema = NestedSchema(); - config.ValidateResult = result => - { - result.Errors.Count.ShouldBe(1); - result.Errors[0].Message.ShouldBe(@"You are not authorized to run this query. + result.Errors.Count.ShouldBe(1); + result.Errors[0].Message.ShouldBe(@"You are not authorized to run this query. Required claim 'admin' is not present."); - }; - }); - } + }; + }); + } - [Fact] - public void passes_with_claim_on_input_type() - { - ConfigureAuthorizationOptions(options => options.AddPolicy("FieldPolicy", x => x.RequireClaim("admin"))); + [Fact] + public void passes_with_claim_on_input_type() + { + ConfigureAuthorizationOptions(options => options.AddPolicy("FieldPolicy", x => x.RequireClaim("admin"))); - ShouldPassRule(config => + ShouldPassRule(config => + { + config.Query = @"query { author(input: { name: ""Quinn"" }) }"; + config.Schema = TypedSchema(); + config.User = CreatePrincipal(claims: new Dictionary { - config.Query = @"query { author(input: { name: ""Quinn"" }) }"; - config.Schema = TypedSchema(); - config.User = CreatePrincipal(claims: new Dictionary - { - { "Admin", "true" } - }); + { "Admin", "true" } }); - } + }); + } - [Fact] - public void nested_type_list_policy_fail() - { - ConfigureAuthorizationOptions(options => options.AddPolicy("PostPolicy", x => x.RequireClaim("admin"))); + [Fact] + public void nested_type_list_policy_fail() + { + ConfigureAuthorizationOptions(options => options.AddPolicy("PostPolicy", x => x.RequireClaim("admin"))); - ShouldFailRule(config => + ShouldFailRule(config => + { + config.Query = @"query { posts }"; + config.Schema = NestedSchema(); + config.ValidateResult = result => { - config.Query = @"query { posts }"; - config.Schema = NestedSchema(); - config.ValidateResult = result => - { - result.Errors.Count.ShouldBe(1); - result.Errors[0].Message.ShouldBe(@"You are not authorized to run this query. + result.Errors.Count.ShouldBe(1); + result.Errors[0].Message.ShouldBe(@"You are not authorized to run this query. Required claim 'admin' is not present."); - }; - }); - } + }; + }); + } - [Fact] - public void nested_type_list_non_null_policy_fail() - { - ConfigureAuthorizationOptions(options => options.AddPolicy("PostPolicy", x => x.RequireClaim("admin"))); + [Fact] + public void nested_type_list_non_null_policy_fail() + { + ConfigureAuthorizationOptions(options => options.AddPolicy("PostPolicy", x => x.RequireClaim("admin"))); - ShouldFailRule(config => + ShouldFailRule(config => + { + config.Query = @"query { postsNonNull }"; + config.Schema = NestedSchema(); + config.ValidateResult = result => { - config.Query = @"query { postsNonNull }"; - config.Schema = NestedSchema(); - config.ValidateResult = result => - { - result.Errors.Count.ShouldBe(1); - result.Errors[0].Message.ShouldBe(@"You are not authorized to run this query. + result.Errors.Count.ShouldBe(1); + result.Errors[0].Message.ShouldBe(@"You are not authorized to run this query. Required claim 'admin' is not present."); - }; - }); - } + }; + }); + } - [Fact] - public void fails_on_missing_claim_on_input_type() - { - ConfigureAuthorizationOptions(options => options.AddPolicy("FieldPolicy", x => x.RequireClaim("admin"))); + [Fact] + public void fails_on_missing_claim_on_input_type() + { + ConfigureAuthorizationOptions(options => options.AddPolicy("FieldPolicy", x => x.RequireClaim("admin"))); - ShouldFailRule(config => + ShouldFailRule(config => + { + config.Query = @"query { author(input: { name: ""Quinn"" }) }"; + config.Schema = TypedSchema(); + config.ValidateResult = result => { - config.Query = @"query { author(input: { name: ""Quinn"" }) }"; - config.Schema = TypedSchema(); - config.ValidateResult = result => - { - result.Errors.Count.ShouldBe(1); - result.Errors[0].Message.ShouldBe(@"You are not authorized to run this query. + result.Errors.Count.ShouldBe(1); + result.Errors[0].Message.ShouldBe(@"You are not authorized to run this query. Required claim 'admin' is not present."); - }; - }); - } + }; + }); + } - [Fact] - public void passes_with_policy_on_connection_type() - { - ConfigureAuthorizationOptions(options => options.AddPolicy("ConnectionPolicy", x => x.RequireClaim("admin"))); + [Fact] + public void passes_with_policy_on_connection_type() + { + ConfigureAuthorizationOptions(options => options.AddPolicy("ConnectionPolicy", x => x.RequireClaim("admin"))); - ShouldPassRule(config => + ShouldPassRule(config => + { + config.Query = @"query { posts { items { id } } }"; + config.Schema = TypedSchema(); + config.User = CreatePrincipal(claims: new Dictionary { - config.Query = @"query { posts { items { id } } }"; - config.Schema = TypedSchema(); - config.User = CreatePrincipal(claims: new Dictionary - { - { "Admin", "true" } - }); + { "Admin", "true" } }); - } + }); + } - [Fact] - public void fails_on_missing_claim_on_connection_type() - { - ConfigureAuthorizationOptions(options => options.AddPolicy("ConnectionPolicy", x => x.RequireClaim("admin"))); + [Fact] + public void fails_on_missing_claim_on_connection_type() + { + ConfigureAuthorizationOptions(options => options.AddPolicy("ConnectionPolicy", x => x.RequireClaim("admin"))); - ShouldFailRule(config => + ShouldFailRule(config => + { + config.Query = @"query { posts { items { id } } }"; + config.Schema = TypedSchema(); + config.User = CreatePrincipal(); + config.ValidateResult = result => { - config.Query = @"query { posts { items { id } } }"; - config.Schema = TypedSchema(); - config.User = CreatePrincipal(); - config.ValidateResult = result => - { - result.Errors.Count.ShouldBe(1); - result.Errors[0].Message.ShouldBe(@"You are not authorized to run this query. + result.Errors.Count.ShouldBe(1); + result.Errors[0].Message.ShouldBe(@"You are not authorized to run this query. Required claim 'admin' is not present."); - }; - }); - } + }; + }); + } - private static ISchema BasicSchema() - { - string defs = @" + private static ISchema BasicSchema() + { + string defs = @" type Query { post(id: ID!): String } "; - return Schema.For(defs, _ => _.Types.Include()); - } + return Schema.For(defs, _ => _.Types.Include()); + } - [GraphQLMetadata("Query")] - [Authorize("ClassPolicy")] - public class BasicQueryWithAttributesAndClassPolicy - { - [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "for tests")] - public string Post(string id) => ""; - } + [GraphQLMetadata("Query")] + [Authorize("ClassPolicy")] + public class BasicQueryWithAttributesAndClassPolicy + { + [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "for tests")] + public string Post(string id) => ""; + } - [GraphQLMetadata("Query")] - public class BasicQueryWithAttributesAndMethodPolicy - { - [Authorize("FieldPolicy")] - [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "for tests")] - public string Post(string id) => ""; - } + [GraphQLMetadata("Query")] + public class BasicQueryWithAttributesAndMethodPolicy + { + [Authorize("FieldPolicy")] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "for tests")] + public string Post(string id) => ""; + } - [GraphQLMetadata("Query")] - public class BasicQueryWithAttributesAndPropertyPolicy - { - [Authorize("FieldPolicy")] - public string Post { get; set; } = ""; - } + [GraphQLMetadata("Query")] + public class BasicQueryWithAttributesAndPropertyPolicy + { + [Authorize("FieldPolicy")] + public string Post { get; set; } = ""; + } - private ISchema NestedSchema() - { - string defs = @" + private ISchema NestedSchema() + { + string defs = @" type Query { post(id: ID!): Post posts: [Post] @@ -343,67 +343,66 @@ type Post { } "; - return Schema.For(defs, _ => - { - _.Types.Include(); - _.Types.Include(); - }); - } - - [GraphQLMetadata("Query")] - public class NestedQueryWithAttributes + return Schema.For(defs, _ => { - [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "for tests")] - public Post Post(string id) => null; + _.Types.Include(); + _.Types.Include(); + }); + } - public IEnumerable Posts() => null; + [GraphQLMetadata("Query")] + public class NestedQueryWithAttributes + { + [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "for tests")] + public Post Post(string id) => null; - public IEnumerable PostsNonNull() => null; - } + public IEnumerable Posts() => null; - [Authorize("PostPolicy")] - public class Post - { - public string Id { get; set; } - } + public IEnumerable PostsNonNull() => null; + } - public class PostGraphType : ObjectGraphType - { - public PostGraphType() - { - Field(p => p.Id); - } - } + [Authorize("PostPolicy")] + public class Post + { + public string Id { get; set; } + } - public class Author + public class PostGraphType : ObjectGraphType + { + public PostGraphType() { - public string Name { get; set; } + Field(p => p.Id); } + } - private ISchema TypedSchema() - { - var query = new ObjectGraphType(); + public class Author + { + public string Name { get; set; } + } - query.Field( - "author", - arguments: new QueryArguments(new QueryArgument { Name = "input" }), - resolve: context => "testing" - ); + private ISchema TypedSchema() + { + var query = new ObjectGraphType(); - query.Connection() - .Name("posts") - .AuthorizeWithPolicy("ConnectionPolicy") - .Resolve(ctx => new Connection()); + query.Field( + "author", + arguments: new QueryArguments(new QueryArgument { Name = "input" }), + resolve: context => "testing" + ); - return new Schema { Query = query }; - } + query.Connection() + .Name("posts") + .AuthorizeWithPolicy("ConnectionPolicy") + .Resolve(ctx => new Connection()); - public class AuthorInputType : InputObjectGraphType + return new Schema { Query = query }; + } + + public class AuthorInputType : InputObjectGraphType + { + public AuthorInputType() { - public AuthorInputType() - { - Field(x => x.Name).AuthorizeWithPolicy("FieldPolicy"); - } + Field(x => x.Name).AuthorizeWithPolicy("FieldPolicy"); } } } diff --git a/tests/Authorization.AspNetCore.Tests/ValidationTestBase.cs b/tests/Authorization.AspNetCore.Tests/ValidationTestBase.cs index 1e6736cf..38bcaf1f 100644 --- a/tests/Authorization.AspNetCore.Tests/ValidationTestBase.cs +++ b/tests/Authorization.AspNetCore.Tests/ValidationTestBase.cs @@ -6,112 +6,111 @@ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; -namespace GraphQL.Server.Authorization.AspNetCore.Tests -{ - public class ValidationTestBase : IDisposable - { - protected ServiceProvider ServiceProvider { get; private set; } +namespace GraphQL.Server.Authorization.AspNetCore.Tests; - protected HttpContext HttpContext { get; private set; } +public class ValidationTestBase : IDisposable +{ + protected ServiceProvider ServiceProvider { get; private set; } - protected AuthorizationValidationRule Rule { get; private set; } + protected HttpContext HttpContext { get; private set; } - protected void ConfigureAuthorizationOptions(Action setupOptions) - { - var (authorizationService, httpContextAccessor) = BuildServices(setupOptions); - HttpContext = httpContextAccessor.HttpContext; - Rule = new AuthorizationValidationRule(authorizationService, new DefaultClaimsPrincipalAccessor(httpContextAccessor), new DefaultAuthorizationErrorMessageBuilder()); - } + protected AuthorizationValidationRule Rule { get; private set; } - protected void ShouldPassRule(Action configure) - { - var config = new ValidationTestConfig(); - config.Rules.Add(Rule); - configure(config); + protected void ConfigureAuthorizationOptions(Action setupOptions) + { + var (authorizationService, httpContextAccessor) = BuildServices(setupOptions); + HttpContext = httpContextAccessor.HttpContext; + Rule = new AuthorizationValidationRule(authorizationService, new DefaultClaimsPrincipalAccessor(httpContextAccessor), new DefaultAuthorizationErrorMessageBuilder()); + } - config.Rules.Any().ShouldBeTrue("Must provide at least one rule to validate against."); + protected void ShouldPassRule(Action configure) + { + var config = new ValidationTestConfig(); + config.Rules.Add(Rule); + configure(config); - config.Schema.Initialize(); + config.Rules.Any().ShouldBeTrue("Must provide at least one rule to validate against."); - var result = Validate(config); + config.Schema.Initialize(); - string message = ""; - if (result.Errors?.Any() == true) - { - message = string.Join(", ", result.Errors.Select(x => x.Message)); - } - result.IsValid.ShouldBeTrue(message); - config.ValidateResult(result); - } + var result = Validate(config); - protected void ShouldFailRule(Action configure) + string message = ""; + if (result.Errors?.Any() == true) { - var config = new ValidationTestConfig(); - config.Rules.Add(Rule); - configure(config); + message = string.Join(", ", result.Errors.Select(x => x.Message)); + } + result.IsValid.ShouldBeTrue(message); + config.ValidateResult(result); + } - config.Rules.Any().ShouldBeTrue("Must provide at least one rule to validate against."); + protected void ShouldFailRule(Action configure) + { + var config = new ValidationTestConfig(); + config.Rules.Add(Rule); + configure(config); - config.Schema.Initialize(); + config.Rules.Any().ShouldBeTrue("Must provide at least one rule to validate against."); - var result = Validate(config); + config.Schema.Initialize(); - result.IsValid.ShouldBeFalse("Expected validation errors though there were none."); - config.ValidateResult(result); - } + var result = Validate(config); - private (IAuthorizationService, IHttpContextAccessor) BuildServices(Action setupOptions) - { - if (ServiceProvider != null) - throw new InvalidOperationException("BuildServices has been already called"); + result.IsValid.ShouldBeFalse("Expected validation errors though there were none."); + config.ValidateResult(result); + } - var services = new ServiceCollection() - .AddAuthorization(setupOptions) - .AddLogging() - .AddOptions() - .AddHttpContextAccessor(); + private (IAuthorizationService, IHttpContextAccessor) BuildServices(Action setupOptions) + { + if (ServiceProvider != null) + throw new InvalidOperationException("BuildServices has been already called"); - ServiceProvider = services.BuildServiceProvider(); + var services = new ServiceCollection() + .AddAuthorization(setupOptions) + .AddLogging() + .AddOptions() + .AddHttpContextAccessor(); - var authorizationService = ServiceProvider.GetRequiredService(); - var httpContextAccessor = ServiceProvider.GetRequiredService(); + ServiceProvider = services.BuildServiceProvider(); - httpContextAccessor.HttpContext = new DefaultHttpContext(); - return (authorizationService, httpContextAccessor); - } + var authorizationService = ServiceProvider.GetRequiredService(); + var httpContextAccessor = ServiceProvider.GetRequiredService(); - private IValidationResult Validate(ValidationTestConfig config) - { - HttpContext.User = config.User; - var documentBuilder = new GraphQLDocumentBuilder(); - var document = documentBuilder.Build(config.Query); - var validator = new DocumentValidator(); - return validator.ValidateAsync(new ValidationOptions - { - Schema = config.Schema, - Document = document, - Operation = document.Definitions.OfType().First(), - Rules = config.Rules, - Variables = config.Inputs - }).GetAwaiter().GetResult().validationResult; - } + httpContextAccessor.HttpContext = new DefaultHttpContext(); + return (authorizationService, httpContextAccessor); + } - protected ClaimsPrincipal CreatePrincipal(string authenticationType = null, IDictionary claims = null) + private IValidationResult Validate(ValidationTestConfig config) + { + HttpContext.User = config.User; + var documentBuilder = new GraphQLDocumentBuilder(); + var document = documentBuilder.Build(config.Query); + var validator = new DocumentValidator(); + return validator.ValidateAsync(new ValidationOptions { - var claimsList = new List(); - - if (claims != null) - { - foreach (var c in claims) - claimsList.Add(new Claim(c.Key, c.Value)); - } + Schema = config.Schema, + Document = document, + Operation = document.Definitions.OfType().First(), + Rules = config.Rules, + Variables = config.Inputs + }).GetAwaiter().GetResult().validationResult; + } - return new ClaimsPrincipal(new ClaimsIdentity(claimsList, authenticationType)); - } + protected ClaimsPrincipal CreatePrincipal(string authenticationType = null, IDictionary claims = null) + { + var claimsList = new List(); - public void Dispose() + if (claims != null) { - ServiceProvider.Dispose(); + foreach (var c in claims) + claimsList.Add(new Claim(c.Key, c.Value)); } + + return new ClaimsPrincipal(new ClaimsIdentity(claimsList, authenticationType)); + } + + public void Dispose() + { + ServiceProvider.Dispose(); } } diff --git a/tests/Authorization.AspNetCore.Tests/ValidationTestConfig.cs b/tests/Authorization.AspNetCore.Tests/ValidationTestConfig.cs index 3a842d93..394c5dc3 100644 --- a/tests/Authorization.AspNetCore.Tests/ValidationTestConfig.cs +++ b/tests/Authorization.AspNetCore.Tests/ValidationTestConfig.cs @@ -2,20 +2,19 @@ using GraphQL.Types; using GraphQL.Validation; -namespace GraphQL.Server.Authorization.AspNetCore.Tests +namespace GraphQL.Server.Authorization.AspNetCore.Tests; + +public class ValidationTestConfig { - public class ValidationTestConfig - { - public string Query { get; set; } + public string Query { get; set; } - public ISchema Schema { get; set; } + public ISchema Schema { get; set; } - public List Rules { get; set; } = new List(); + public List Rules { get; set; } = new List(); - public ClaimsPrincipal User { get; set; } + public ClaimsPrincipal User { get; set; } - public Inputs Inputs { get; set; } + public Inputs Inputs { get; set; } - public Action ValidateResult = _ => { }; - } + public Action ValidateResult = _ => { }; } diff --git a/tests/Samples.Server.Tests/BaseTest.cs b/tests/Samples.Server.Tests/BaseTest.cs index 9db5bfef..11776da0 100644 --- a/tests/Samples.Server.Tests/BaseTest.cs +++ b/tests/Samples.Server.Tests/BaseTest.cs @@ -6,133 +6,132 @@ using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.Hosting; -namespace Samples.Server.Tests -{ - public abstract class BaseTest : IDisposable - { - private const string GRAPHQL_URL = "graphql"; +namespace Samples.Server.Tests; - protected BaseTest() - { - Host = Program.CreateHostBuilder(Array.Empty()) - .ConfigureWebHost(webBuilder => webBuilder.UseTestServer()) - .Start(); +public abstract class BaseTest : IDisposable +{ + private const string GRAPHQL_URL = "graphql"; - Server = Host.GetTestServer(); - Client = Server.CreateClient(); - } + protected BaseTest() + { + Host = Program.CreateHostBuilder(Array.Empty()) + .ConfigureWebHost(webBuilder => webBuilder.UseTestServer()) + .Start(); - protected Task SendRequestAsync(HttpMethod httpMethod, HttpContent httpContent) - { - var request = new HttpRequestMessage(httpMethod, GRAPHQL_URL) - { - Content = httpContent - }; - return Client.SendAsync(request); - } + Server = Host.GetTestServer(); + Client = Server.CreateClient(); + } - /// - /// Sends a gql request to the server. - /// - /// Request details. - /// Request type. - /// - /// Optional override request details to be passed via the URL query string. - /// Used to facilitate testing of query string values over body content. - /// - /// - /// Raw response as a string of JSON. - /// - protected async Task SendRequestAsync(GraphQLRequest request, RequestType requestType, - GraphQLRequest queryStringOverride = null) + protected Task SendRequestAsync(HttpMethod httpMethod, HttpContent httpContent) + { + var request = new HttpRequestMessage(httpMethod, GRAPHQL_URL) { - // Different servings over HTTP: - // https://graphql.org/learn/serving-over-http/ - // https://github.com/graphql/express-graphql/blob/master/src/index.js - - // Build a url to call the api with - string url = GRAPHQL_URL; + Content = httpContent + }; + return Client.SendAsync(request); + } - // If query string override request details are provided, - // use it where valid. For PostWithGraph, this is handled in its own part of the next - // switch statement as it needs to pass its own query strings for just the `request`. - if (queryStringOverride != null && requestType != RequestType.PostWithGraph) - { - if (requestType == RequestType.Get) - { - throw new ArgumentException( - $"It's not valid to set a {nameof(queryStringOverride)} " + - $"with a {nameof(requestType)} of {requestType} as the {nameof(request)} " + - $"will already be set in the querystring", - nameof(queryStringOverride)); - } - - url += "?" + await Serializer.ToQueryStringParamsAsync(queryStringOverride); - } + /// + /// Sends a gql request to the server. + /// + /// Request details. + /// Request type. + /// + /// Optional override request details to be passed via the URL query string. + /// Used to facilitate testing of query string values over body content. + /// + /// + /// Raw response as a string of JSON. + /// + protected async Task SendRequestAsync(GraphQLRequest request, RequestType requestType, + GraphQLRequest queryStringOverride = null) + { + // Different servings over HTTP: + // https://graphql.org/learn/serving-over-http/ + // https://github.com/graphql/express-graphql/blob/master/src/index.js - string urlWithParams; - HttpResponseMessage response; + // Build a url to call the api with + string url = GRAPHQL_URL; - // Handle different request types as necessary - switch (requestType) + // If query string override request details are provided, + // use it where valid. For PostWithGraph, this is handled in its own part of the next + // switch statement as it needs to pass its own query strings for just the `request`. + if (queryStringOverride != null && requestType != RequestType.PostWithGraph) + { + if (requestType == RequestType.Get) { - case RequestType.Get: - // Details passed in query string - urlWithParams = url + "?" + await Serializer.ToQueryStringParamsAsync(request); - response = await Client.GetAsync(urlWithParams); - break; - - case RequestType.PostWithJson: - // Details passed in body content as JSON, with url query string params also allowed - string json = Serializer.ToJson(request); - var jsonContent = new StringContent(json, Encoding.UTF8, MediaType.JSON); - response = await Client.PostAsync(url, jsonContent); - break; - - case RequestType.PostWithGraph: - // Query in body content (raw), operationName and variables in query string params, - // but take the overrides as a priority to facilitate the tests that use it - urlWithParams = GRAPHQL_URL + "?" + await Serializer.ToQueryStringParamsAsync(new GraphQLRequest - { - Query = queryStringOverride?.Query, - OperationName = queryStringOverride?.OperationName ?? request.OperationName, - Variables = queryStringOverride?.Variables ?? request.Variables - }); - var graphContent = new StringContent(request.Query, Encoding.UTF8, MediaType.GRAPH_QL); - response = await Client.PostAsync(urlWithParams, graphContent); - break; - - case RequestType.PostWithForm: - // Details passed in form body as form url encoded, with url query string params also allowed - var formContent = Serializer.ToFormUrlEncodedContent(request); - response = await Client.PostAsync(url, formContent); - break; - - default: - throw new NotImplementedException(); + throw new ArgumentException( + $"It's not valid to set a {nameof(queryStringOverride)} " + + $"with a {nameof(requestType)} of {requestType} as the {nameof(request)} " + + $"will already be set in the querystring", + nameof(queryStringOverride)); } - return await response.Content.ReadAsStringAsync(); + url += "?" + await Serializer.ToQueryStringParamsAsync(queryStringOverride); } - protected async Task SendBatchRequestAsync(params GraphQLRequest[] requests) - { - string content = Serializer.ToJson(requests); - using var response = await Client.PostAsync("graphql", new StringContent(content, Encoding.UTF8, "application/json")); - return await response.Content.ReadAsStringAsync(); - } + string urlWithParams; + HttpResponseMessage response; - public virtual void Dispose() + // Handle different request types as necessary + switch (requestType) { - Client.Dispose(); - Server.Dispose(); - Host.Dispose(); + case RequestType.Get: + // Details passed in query string + urlWithParams = url + "?" + await Serializer.ToQueryStringParamsAsync(request); + response = await Client.GetAsync(urlWithParams); + break; + + case RequestType.PostWithJson: + // Details passed in body content as JSON, with url query string params also allowed + string json = Serializer.ToJson(request); + var jsonContent = new StringContent(json, Encoding.UTF8, MediaType.JSON); + response = await Client.PostAsync(url, jsonContent); + break; + + case RequestType.PostWithGraph: + // Query in body content (raw), operationName and variables in query string params, + // but take the overrides as a priority to facilitate the tests that use it + urlWithParams = GRAPHQL_URL + "?" + await Serializer.ToQueryStringParamsAsync(new GraphQLRequest + { + Query = queryStringOverride?.Query, + OperationName = queryStringOverride?.OperationName ?? request.OperationName, + Variables = queryStringOverride?.Variables ?? request.Variables + }); + var graphContent = new StringContent(request.Query, Encoding.UTF8, MediaType.GRAPH_QL); + response = await Client.PostAsync(urlWithParams, graphContent); + break; + + case RequestType.PostWithForm: + // Details passed in form body as form url encoded, with url query string params also allowed + var formContent = Serializer.ToFormUrlEncodedContent(request); + response = await Client.PostAsync(url, formContent); + break; + + default: + throw new NotImplementedException(); } - private TestServer Server { get; } + return await response.Content.ReadAsStringAsync(); + } - private HttpClient Client { get; } + protected async Task SendBatchRequestAsync(params GraphQLRequest[] requests) + { + string content = Serializer.ToJson(requests); + using var response = await Client.PostAsync("graphql", new StringContent(content, Encoding.UTF8, "application/json")); + return await response.Content.ReadAsStringAsync(); + } - private IHost Host { get; } + public virtual void Dispose() + { + Client.Dispose(); + Server.Dispose(); + Host.Dispose(); } + + private TestServer Server { get; } + + private HttpClient Client { get; } + + private IHost Host { get; } } diff --git a/tests/Samples.Server.Tests/DITest.cs b/tests/Samples.Server.Tests/DITest.cs index cec1f510..dbffd7bf 100644 --- a/tests/Samples.Server.Tests/DITest.cs +++ b/tests/Samples.Server.Tests/DITest.cs @@ -4,21 +4,20 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -namespace Samples.Server.Tests +namespace Samples.Server.Tests; + +public class DITest { - public class DITest + [Fact] + public void Services_Should_Contain_Only_One_DocumentWriter() { - [Fact] - public void Services_Should_Contain_Only_One_DocumentWriter() - { - var cfg = new ConfigurationBuilder().Build(); - var env = (IWebHostEnvironment)Activator.CreateInstance(Type.GetType("Microsoft.AspNetCore.Hosting.HostingEnvironment, Microsoft.AspNetCore.Hosting")); - var startup = new Startup(cfg, env); - var services = new ServiceCollection(); - startup.ConfigureServices(services); - var provider = services.BuildServiceProvider(); - var serializers = provider.GetServices(); - serializers.Count().ShouldBe(1); - } + var cfg = new ConfigurationBuilder().Build(); + var env = (IWebHostEnvironment)Activator.CreateInstance(Type.GetType("Microsoft.AspNetCore.Hosting.HostingEnvironment, Microsoft.AspNetCore.Hosting")); + var startup = new Startup(cfg, env); + var services = new ServiceCollection(); + startup.ConfigureServices(services); + var provider = services.BuildServiceProvider(); + var serializers = provider.GetServices(); + serializers.Count().ShouldBe(1); } } diff --git a/tests/Samples.Server.Tests/RequestType.cs b/tests/Samples.Server.Tests/RequestType.cs index f50ba8aa..119fd5cd 100644 --- a/tests/Samples.Server.Tests/RequestType.cs +++ b/tests/Samples.Server.Tests/RequestType.cs @@ -1,14 +1,13 @@ -namespace Samples.Server.Tests +namespace Samples.Server.Tests; + +/// +/// Different types of HTTP requests a GraphQL HTTP server should be able to understand. +/// See: https://graphql.org/learn/serving-over-http/ +/// +public enum RequestType { - /// - /// Different types of HTTP requests a GraphQL HTTP server should be able to understand. - /// See: https://graphql.org/learn/serving-over-http/ - /// - public enum RequestType - { - Get, - PostWithJson, - PostWithForm, - PostWithGraph, - } + Get, + PostWithJson, + PostWithForm, + PostWithGraph, } diff --git a/tests/Samples.Server.Tests/ResponseTests.cs b/tests/Samples.Server.Tests/ResponseTests.cs index e5208475..6b9ccb31 100644 --- a/tests/Samples.Server.Tests/ResponseTests.cs +++ b/tests/Samples.Server.Tests/ResponseTests.cs @@ -3,219 +3,218 @@ using GraphQL.Server; using GraphQL.Transport; -namespace Samples.Server.Tests +namespace Samples.Server.Tests; + +public class ResponseTests : BaseTest { - public class ResponseTests : BaseTest + [Theory] + [InlineData(RequestType.Get)] + [InlineData(RequestType.PostWithJson)] + [InlineData(RequestType.PostWithGraph)] + [InlineData(RequestType.PostWithForm)] + public async Task Single_Query_Should_Return_Single_Result(RequestType requestType) { - [Theory] - [InlineData(RequestType.Get)] - [InlineData(RequestType.PostWithJson)] - [InlineData(RequestType.PostWithGraph)] - [InlineData(RequestType.PostWithForm)] - public async Task Single_Query_Should_Return_Single_Result(RequestType requestType) - { - var request = new GraphQLRequest { Query = "{ __schema { queryType { name } } }" }; - string response = await SendRequestAsync(request, requestType); - response.ShouldBeEquivalentJson(@"{""data"":{""__schema"":{""queryType"":{""name"":""ChatQuery""}}}}", ignoreExtensions: true); - } - - /// - /// Tests that POST type requests are overridden by query string params. - /// - [Theory] - [InlineData(RequestType.PostWithJson)] - [InlineData(RequestType.PostWithGraph)] - [InlineData(RequestType.PostWithForm)] - public async Task Middleware_Should_Prioritise_Query_String_Values(RequestType requestType) - { - var request = new GraphQLRequest - { - Query = "mutation one ($content: String!, $fromId: String!, $sentAt: Date!) { addMessage(message: { content: $content, fromId: $fromId, sentAt: $sentAt }) { sentAt, content, from { id } } }", - Variables = @"{ ""content"": ""one content"", ""sentAt"": ""2020-01-01"", ""fromId"": ""1"" }".ToInputs(), - OperationName = "one" - }; - - var requestB = new GraphQLRequest - { - Query = "mutation two ($content: String!, $fromId: String!, $sentAt: Date!) { addMessage(message: { content: $content, fromId: $fromId, sentAt: $sentAt }) { sentAt, content, from { id } } }", - Variables = @"{ ""content"": ""two content"", ""sentAt"": ""2020-01-01"", ""fromId"": ""1"" }".ToInputs(), - OperationName = "two" - }; - - string response = await SendRequestAsync(request, requestType, queryStringOverride: requestB); - response.ShouldBeEquivalentJson( - @"{""data"":{""addMessage"":{""sentAt"":""2020-01-01T00:00:00Z"",""content"":""two content"",""from"":{""id"":""1""}}}}", - ignoreExtensions: true); - } - - [Fact] - public async Task Batched_Query_Should_Return_Multiple_Results() - { - string response = await SendBatchRequestAsync( - new GraphQLRequest { Query = "query one { __schema { queryType { name } } }", OperationName = "one" }, - new GraphQLRequest { Query = "query two { __schema { queryType { name } } }", OperationName = "two" }, - new GraphQLRequest { Query = "query three { __schema { queryType { name } } }", OperationName = "three" } - ); - response.ShouldBeEquivalentJson(@"[{""data"":{""__schema"":{""queryType"":{""name"":""ChatQuery""}}}},{""data"":{""__schema"":{""queryType"":{""name"":""ChatQuery""}}}},{""data"":{""__schema"":{""queryType"":{""name"":""ChatQuery""}}}}]", ignoreExtensions: true); - } - - [Fact] - public async Task Batched_Query_Should_Return_Single_Result_As_Array() + var request = new GraphQLRequest { Query = "{ __schema { queryType { name } } }" }; + string response = await SendRequestAsync(request, requestType); + response.ShouldBeEquivalentJson(@"{""data"":{""__schema"":{""queryType"":{""name"":""ChatQuery""}}}}", ignoreExtensions: true); + } + + /// + /// Tests that POST type requests are overridden by query string params. + /// + [Theory] + [InlineData(RequestType.PostWithJson)] + [InlineData(RequestType.PostWithGraph)] + [InlineData(RequestType.PostWithForm)] + public async Task Middleware_Should_Prioritise_Query_String_Values(RequestType requestType) + { + var request = new GraphQLRequest { - string response = await SendBatchRequestAsync( - new GraphQLRequest { Query = "query one { __schema { queryType { name } } }", OperationName = "one" } - ); - response.ShouldBeEquivalentJson(@"[{""data"":{""__schema"":{""queryType"":{""name"":""ChatQuery""}}}}]", ignoreExtensions: true); - } - - [Theory] - [MemberData(nameof(WrongQueryData))] - public async Task Wrong_Query_Should_Return_Error(HttpMethod httpMethod, HttpContent httpContent, - HttpStatusCode expectedStatusCode, string expectedErrorMsg) + Query = "mutation one ($content: String!, $fromId: String!, $sentAt: Date!) { addMessage(message: { content: $content, fromId: $fromId, sentAt: $sentAt }) { sentAt, content, from { id } } }", + Variables = @"{ ""content"": ""one content"", ""sentAt"": ""2020-01-01"", ""fromId"": ""1"" }".ToInputs(), + OperationName = "one" + }; + + var requestB = new GraphQLRequest { - var response = await SendRequestAsync(httpMethod, httpContent); - string expected = @"{""errors"":[{""message"":""" + expectedErrorMsg + @"""}]}"; + Query = "mutation two ($content: String!, $fromId: String!, $sentAt: Date!) { addMessage(message: { content: $content, fromId: $fromId, sentAt: $sentAt }) { sentAt, content, from { id } } }", + Variables = @"{ ""content"": ""two content"", ""sentAt"": ""2020-01-01"", ""fromId"": ""1"" }".ToInputs(), + OperationName = "two" + }; - response.StatusCode.ShouldBe(expectedStatusCode); + string response = await SendRequestAsync(request, requestType, queryStringOverride: requestB); + response.ShouldBeEquivalentJson( + @"{""data"":{""addMessage"":{""sentAt"":""2020-01-01T00:00:00Z"",""content"":""two content"",""from"":{""id"":""1""}}}}", + ignoreExtensions: true); + } - string content = await response.Content.ReadAsStringAsync(); - content.ShouldBeEquivalentJson(expected); - } + [Fact] + public async Task Batched_Query_Should_Return_Multiple_Results() + { + string response = await SendBatchRequestAsync( + new GraphQLRequest { Query = "query one { __schema { queryType { name } } }", OperationName = "one" }, + new GraphQLRequest { Query = "query two { __schema { queryType { name } } }", OperationName = "two" }, + new GraphQLRequest { Query = "query three { __schema { queryType { name } } }", OperationName = "three" } + ); + response.ShouldBeEquivalentJson(@"[{""data"":{""__schema"":{""queryType"":{""name"":""ChatQuery""}}}},{""data"":{""__schema"":{""queryType"":{""name"":""ChatQuery""}}}},{""data"":{""__schema"":{""queryType"":{""name"":""ChatQuery""}}}}]", ignoreExtensions: true); + } + + [Fact] + public async Task Batched_Query_Should_Return_Single_Result_As_Array() + { + string response = await SendBatchRequestAsync( + new GraphQLRequest { Query = "query one { __schema { queryType { name } } }", OperationName = "one" } + ); + response.ShouldBeEquivalentJson(@"[{""data"":{""__schema"":{""queryType"":{""name"":""ChatQuery""}}}}]", ignoreExtensions: true); + } + + [Theory] + [MemberData(nameof(WrongQueryData))] + public async Task Wrong_Query_Should_Return_Error(HttpMethod httpMethod, HttpContent httpContent, + HttpStatusCode expectedStatusCode, string expectedErrorMsg) + { + var response = await SendRequestAsync(httpMethod, httpContent); + string expected = @"{""errors"":[{""message"":""" + expectedErrorMsg + @"""}]}"; + + response.StatusCode.ShouldBe(expectedStatusCode); + + string content = await response.Content.ReadAsStringAsync(); + content.ShouldBeEquivalentJson(expected); + } - public static IEnumerable WrongQueryData => new object[][] + public static IEnumerable WrongQueryData => new object[][] + { + // Methods other than GET or POST shouldn't be allowed + new object[] + { + HttpMethod.Put, + new StringContent(Serializer.ToJson(new GraphQLRequest { Query = "query { __schema { queryType { name } } }" }), Encoding.UTF8, "application/json"), + HttpStatusCode.MethodNotAllowed, + "Invalid HTTP method. Only GET and POST are supported. See: http://graphql.org/learn/serving-over-http/.", + }, + + // POST with unsupported mime type should be a unsupported media type + new object[] + { + HttpMethod.Post, + new StringContent(Serializer.ToJson(new GraphQLRequest { Query = "query { __schema { queryType { name } } }" }), Encoding.UTF8, "something/unknown"), + HttpStatusCode.UnsupportedMediaType, + "Invalid 'Content-Type' header: non-supported media type 'something/unknown; charset=utf-8'. Must be of 'application/json', 'application/graphql' or 'application/x-www-form-urlencoded'. See: http://graphql.org/learn/serving-over-http/." + }, + + // MediaTypeHeaderValue ctor throws exception + // POST with unsupported charset should be a unsupported media type + //new object[] + //{ + // HttpMethod.Post, + // new StringContent(Serializer.ToJson(new GraphQLRequest { Query = "query { __schema { queryType { name } } }" }), Encoding.UTF8, "application/json; charset=utf-3"), + // HttpStatusCode.UnsupportedMediaType, + // "Invalid 'Content-Type' header: non-supported media type 'application/json; charset=utf-3'. Must be of 'application/json', 'application/graphql' or 'application/x-www-form-urlencoded'. See: http://graphql.org/learn/serving-over-http/." + //}, + + // POST with JSON mime type that doesn't start with an object or array token should be a bad request + new object[] + { + HttpMethod.Post, + new StringContent("Oops", Encoding.UTF8, "application/json"), + HttpStatusCode.BadRequest, + "JSON body text could not be parsed. 'O' is an invalid start of a value. Path: $ | LineNumber: 0 | BytePositionInLine: 0." + }, + + // POST with JSON mime type that is invalid JSON should be a bad request + new object[] + { + HttpMethod.Post, + new StringContent("{oops}", Encoding.UTF8, "application/json"), + HttpStatusCode.BadRequest, + "JSON body text could not be parsed. 'o' is an invalid start of a property name. Expected a '\"'. Path: $ | LineNumber: 0 | BytePositionInLine: 1." + }, + + // POST with JSON mime type that is null JSON should be a bad request + new object[] { - // Methods other than GET or POST shouldn't be allowed - new object[] - { - HttpMethod.Put, - new StringContent(Serializer.ToJson(new GraphQLRequest { Query = "query { __schema { queryType { name } } }" }), Encoding.UTF8, "application/json"), - HttpStatusCode.MethodNotAllowed, - "Invalid HTTP method. Only GET and POST are supported. See: http://graphql.org/learn/serving-over-http/.", - }, - - // POST with unsupported mime type should be a unsupported media type - new object[] - { - HttpMethod.Post, - new StringContent(Serializer.ToJson(new GraphQLRequest { Query = "query { __schema { queryType { name } } }" }), Encoding.UTF8, "something/unknown"), - HttpStatusCode.UnsupportedMediaType, - "Invalid 'Content-Type' header: non-supported media type 'something/unknown; charset=utf-8'. Must be of 'application/json', 'application/graphql' or 'application/x-www-form-urlencoded'. See: http://graphql.org/learn/serving-over-http/." - }, - - // MediaTypeHeaderValue ctor throws exception - // POST with unsupported charset should be a unsupported media type - //new object[] - //{ - // HttpMethod.Post, - // new StringContent(Serializer.ToJson(new GraphQLRequest { Query = "query { __schema { queryType { name } } }" }), Encoding.UTF8, "application/json; charset=utf-3"), - // HttpStatusCode.UnsupportedMediaType, - // "Invalid 'Content-Type' header: non-supported media type 'application/json; charset=utf-3'. Must be of 'application/json', 'application/graphql' or 'application/x-www-form-urlencoded'. See: http://graphql.org/learn/serving-over-http/." - //}, - - // POST with JSON mime type that doesn't start with an object or array token should be a bad request - new object[] - { - HttpMethod.Post, - new StringContent("Oops", Encoding.UTF8, "application/json"), - HttpStatusCode.BadRequest, - "JSON body text could not be parsed. 'O' is an invalid start of a value. Path: $ | LineNumber: 0 | BytePositionInLine: 0." - }, - - // POST with JSON mime type that is invalid JSON should be a bad request - new object[] - { - HttpMethod.Post, - new StringContent("{oops}", Encoding.UTF8, "application/json"), - HttpStatusCode.BadRequest, - "JSON body text could not be parsed. 'o' is an invalid start of a property name. Expected a '\"'. Path: $ | LineNumber: 0 | BytePositionInLine: 1." - }, - - // POST with JSON mime type that is null JSON should be a bad request - new object[] - { - HttpMethod.Post, - new StringContent("null", Encoding.UTF8, "application/json"), - HttpStatusCode.BadRequest, - "GraphQL query is missing." - }, - - // GET with an empty QueryString should be a bad request - new object[] - { - HttpMethod.Get, - null, - HttpStatusCode.BadRequest, - "GraphQL query is missing." - }, + HttpMethod.Post, + new StringContent("null", Encoding.UTF8, "application/json"), + HttpStatusCode.BadRequest, + "GraphQL query is missing." + }, + + // GET with an empty QueryString should be a bad request + new object[] + { + HttpMethod.Get, + null, + HttpStatusCode.BadRequest, + "GraphQL query is missing." + }, + }; + + [Theory] + [InlineData(RequestType.Get)] + [InlineData(RequestType.PostWithJson)] + [InlineData(RequestType.PostWithGraph)] + [InlineData(RequestType.PostWithForm)] + public async Task Serializer_Should_Handle_Inline_Variables(RequestType requestType) + { + var request = new GraphQLRequest + { + Query = @"mutation { addMessage(message: { content: ""some content"", fromId: ""1"", sentAt: ""2020-01-01"" }) { sentAt, content, from { id } } }" }; + string response = await SendRequestAsync(request, requestType); + response.ShouldBeEquivalentJson( + @"{""data"":{""addMessage"":{""sentAt"":""2020-01-01T00:00:00Z"",""content"":""some content"",""from"":{""id"":""1""}}}}", + ignoreExtensions: true); + } - [Theory] - [InlineData(RequestType.Get)] - [InlineData(RequestType.PostWithJson)] - [InlineData(RequestType.PostWithGraph)] - [InlineData(RequestType.PostWithForm)] - public async Task Serializer_Should_Handle_Inline_Variables(RequestType requestType) - { - var request = new GraphQLRequest - { - Query = @"mutation { addMessage(message: { content: ""some content"", fromId: ""1"", sentAt: ""2020-01-01"" }) { sentAt, content, from { id } } }" - }; - string response = await SendRequestAsync(request, requestType); - response.ShouldBeEquivalentJson( - @"{""data"":{""addMessage"":{""sentAt"":""2020-01-01T00:00:00Z"",""content"":""some content"",""from"":{""id"":""1""}}}}", - ignoreExtensions: true); - } - - [Theory] - [InlineData(RequestType.Get)] - [InlineData(RequestType.PostWithJson)] - [InlineData(RequestType.PostWithGraph)] - [InlineData(RequestType.PostWithForm)] - public async Task Serializer_Should_Handle_Variables(RequestType requestType) + [Theory] + [InlineData(RequestType.Get)] + [InlineData(RequestType.PostWithJson)] + [InlineData(RequestType.PostWithGraph)] + [InlineData(RequestType.PostWithForm)] + public async Task Serializer_Should_Handle_Variables(RequestType requestType) + { + var request = new GraphQLRequest { - var request = new GraphQLRequest - { - Query = "mutation ($content: String!, $fromId: String!, $sentAt: Date!) { addMessage(message: { content: $content, fromId: $fromId, sentAt: $sentAt }) { sentAt, content, from { id } } }", - Variables = @"{ ""content"": ""some content"", ""sentAt"": ""2020-01-01"", ""fromId"": ""1"" }".ToInputs() - }; - string response = await SendRequestAsync(request, requestType); - response.ShouldBeEquivalentJson( - @"{""data"":{""addMessage"":{""sentAt"":""2020-01-01T00:00:00Z"",""content"":""some content"",""from"":{""id"":""1""}}}}", - ignoreExtensions: true); - } - - [Theory] - [InlineData(RequestType.Get)] - [InlineData(RequestType.PostWithJson)] - [InlineData(RequestType.PostWithGraph)] - [InlineData(RequestType.PostWithForm)] - public async Task Serializer_Should_Handle_Complex_Variable(RequestType requestType) + Query = "mutation ($content: String!, $fromId: String!, $sentAt: Date!) { addMessage(message: { content: $content, fromId: $fromId, sentAt: $sentAt }) { sentAt, content, from { id } } }", + Variables = @"{ ""content"": ""some content"", ""sentAt"": ""2020-01-01"", ""fromId"": ""1"" }".ToInputs() + }; + string response = await SendRequestAsync(request, requestType); + response.ShouldBeEquivalentJson( + @"{""data"":{""addMessage"":{""sentAt"":""2020-01-01T00:00:00Z"",""content"":""some content"",""from"":{""id"":""1""}}}}", + ignoreExtensions: true); + } + + [Theory] + [InlineData(RequestType.Get)] + [InlineData(RequestType.PostWithJson)] + [InlineData(RequestType.PostWithGraph)] + [InlineData(RequestType.PostWithForm)] + public async Task Serializer_Should_Handle_Complex_Variable(RequestType requestType) + { + var request = new GraphQLRequest { - var request = new GraphQLRequest - { - Query = "mutation ($msg: MessageInputType!) { addMessage(message: $msg) { sentAt, content, from { id } } }", - Variables = @"{ ""msg"": { ""content"": ""some content"", ""sentAt"": ""2020-01-01"", ""fromId"": ""1"" } }".ToInputs() - }; - string response = await SendRequestAsync(request, requestType); - response.ShouldBeEquivalentJson( - @"{""data"":{""addMessage"":{""sentAt"":""2020-01-01T00:00:00Z"",""content"":""some content"",""from"":{""id"":""1""}}}}", - ignoreExtensions: true); - } - - [Theory] - [InlineData(RequestType.Get)] - [InlineData(RequestType.PostWithJson)] - [InlineData(RequestType.PostWithGraph)] - [InlineData(RequestType.PostWithForm)] - public async Task Serializer_Should_Handle_Empty_Variables(RequestType requestType) + Query = "mutation ($msg: MessageInputType!) { addMessage(message: $msg) { sentAt, content, from { id } } }", + Variables = @"{ ""msg"": { ""content"": ""some content"", ""sentAt"": ""2020-01-01"", ""fromId"": ""1"" } }".ToInputs() + }; + string response = await SendRequestAsync(request, requestType); + response.ShouldBeEquivalentJson( + @"{""data"":{""addMessage"":{""sentAt"":""2020-01-01T00:00:00Z"",""content"":""some content"",""from"":{""id"":""1""}}}}", + ignoreExtensions: true); + } + + [Theory] + [InlineData(RequestType.Get)] + [InlineData(RequestType.PostWithJson)] + [InlineData(RequestType.PostWithGraph)] + [InlineData(RequestType.PostWithForm)] + public async Task Serializer_Should_Handle_Empty_Variables(RequestType requestType) + { + var request = new GraphQLRequest { - var request = new GraphQLRequest - { - Query = "{ __schema { queryType { name } } }", - Variables = "{}".ToInputs() - }; - string response = await SendRequestAsync(request, requestType); - response.ShouldBeEquivalentJson(@"{""data"":{""__schema"":{""queryType"":{""name"":""ChatQuery""}}}}", ignoreExtensions: true); - } + Query = "{ __schema { queryType { name } } }", + Variables = "{}".ToInputs() + }; + string response = await SendRequestAsync(request, requestType); + response.ShouldBeEquivalentJson(@"{""data"":{""__schema"":{""queryType"":{""name"":""ChatQuery""}}}}", ignoreExtensions: true); } } diff --git a/tests/Samples.Server.Tests/Serializer.cs b/tests/Samples.Server.Tests/Serializer.cs index 3e08ebe7..7816583a 100644 --- a/tests/Samples.Server.Tests/Serializer.cs +++ b/tests/Samples.Server.Tests/Serializer.cs @@ -1,56 +1,55 @@ using System.Text.Json; using GraphQL.Transport; -namespace GraphQL.Server -{ - /// - /// Shim and utility methods between our internal representation - /// and how it should serialize over the wire. - /// - internal static class Serializer - { - internal static string ToJson(GraphQLRequest request) - => ToJson(request.ToDictionary()); +namespace GraphQL.Server; - internal static string ToJson(GraphQLRequest[] requests) - => ToJson(Array.ConvertAll(requests, r => r.ToDictionary())); +/// +/// Shim and utility methods between our internal representation +/// and how it should serialize over the wire. +/// +internal static class Serializer +{ + internal static string ToJson(GraphQLRequest request) + => ToJson(request.ToDictionary()); - public static string ToJson(object obj) - => JsonSerializer.Serialize(obj, new JsonSerializerOptions { IgnoreNullValues = true }); + internal static string ToJson(GraphQLRequest[] requests) + => ToJson(Array.ConvertAll(requests, r => r.ToDictionary())); - internal static FormUrlEncodedContent ToFormUrlEncodedContent(GraphQLRequest request) - { - // Don't add keys if `null` as they'll be url encoded as "" or "null" + public static string ToJson(object obj) + => JsonSerializer.Serialize(obj, new JsonSerializerOptions { IgnoreNullValues = true }); - var dictionary = new Dictionary(); - - if (request.OperationName != null) - { - dictionary["operationName"] = request.OperationName; - } + internal static FormUrlEncodedContent ToFormUrlEncodedContent(GraphQLRequest request) + { + // Don't add keys if `null` as they'll be url encoded as "" or "null" - if (request.Query != null) - { - dictionary["query"] = request.Query; - } + var dictionary = new Dictionary(); - if (request.Variables != null) - { - dictionary["variables"] = ToJson(request.Variables); - } + if (request.OperationName != null) + { + dictionary["operationName"] = request.OperationName; + } - return new FormUrlEncodedContent(dictionary); + if (request.Query != null) + { + dictionary["query"] = request.Query; } - internal static Task ToQueryStringParamsAsync(GraphQLRequest request) - => ToFormUrlEncodedContent(request).ReadAsStringAsync(); + if (request.Variables != null) + { + dictionary["variables"] = ToJson(request.Variables); + } - private static Dictionary ToDictionary(this GraphQLRequest request) - => new Dictionary - { - { "operationName", request.OperationName }, - { "query", request.Query }, - { "variables", request.Variables } - }; + return new FormUrlEncodedContent(dictionary); } + + internal static Task ToQueryStringParamsAsync(GraphQLRequest request) + => ToFormUrlEncodedContent(request).ReadAsStringAsync(); + + private static Dictionary ToDictionary(this GraphQLRequest request) + => new Dictionary + { + { "operationName", request.OperationName }, + { "query", request.Query }, + { "variables", request.Variables } + }; } diff --git a/tests/Samples.Server.Tests/ShouldBeExtensions.cs b/tests/Samples.Server.Tests/ShouldBeExtensions.cs index e78ffed0..f2356290 100644 --- a/tests/Samples.Server.Tests/ShouldBeExtensions.cs +++ b/tests/Samples.Server.Tests/ShouldBeExtensions.cs @@ -1,57 +1,56 @@ using Newtonsoft.Json.Linq; -namespace Samples.Server.Tests +namespace Samples.Server.Tests; + +internal static class ShouldBeExtensions { - internal static class ShouldBeExtensions + /// + /// Compares two strings after normalizing any encoding differences first. + /// + /// Actual. + /// Expected. + /// + /// Pass true to ignore the `extensions` path of the JSON when comparing for equality. + /// + public static void ShouldBeEquivalentJson(this string actual, string expected, bool ignoreExtensions) { - /// - /// Compares two strings after normalizing any encoding differences first. - /// - /// Actual. - /// Expected. - /// - /// Pass true to ignore the `extensions` path of the JSON when comparing for equality. - /// - public static void ShouldBeEquivalentJson(this string actual, string expected, bool ignoreExtensions) + if (ignoreExtensions) { - if (ignoreExtensions) + if (actual.StartsWith('[')) { - if (actual.StartsWith('[')) + var json = JArray.Parse(actual); + foreach (var item in json) { - var json = JArray.Parse(actual); - foreach (var item in json) + if (item is JObject obj) { - if (item is JObject obj) - { - obj.Remove("extensions"); - } + obj.Remove("extensions"); } - actual = json.ToString(Newtonsoft.Json.Formatting.None); - } - else - { - var json = JObject.Parse(actual); - json.Remove("extensions"); - actual = json.ToString(Newtonsoft.Json.Formatting.None); } + actual = json.ToString(Newtonsoft.Json.Formatting.None); + } + else + { + var json = JObject.Parse(actual); + json.Remove("extensions"); + actual = json.ToString(Newtonsoft.Json.Formatting.None); } - - actual.ShouldBeEquivalentJson(expected); } - /// - /// Compares two strings after normalizing any encoding differences first. - /// - /// Actual. - /// Expected. - public static void ShouldBeEquivalentJson(this string actualJson, string expectedJson) - { - expectedJson = expectedJson.NormalizeJson(); - actualJson = actualJson.NormalizeJson(); + actual.ShouldBeEquivalentJson(expected); + } - actualJson.ShouldBe(expectedJson); - } + /// + /// Compares two strings after normalizing any encoding differences first. + /// + /// Actual. + /// Expected. + public static void ShouldBeEquivalentJson(this string actualJson, string expectedJson) + { + expectedJson = expectedJson.NormalizeJson(); + actualJson = actualJson.NormalizeJson(); - private static string NormalizeJson(this string json) => json.Replace("'", @"\u0027").Replace("\"", @"\u0022"); + actualJson.ShouldBe(expectedJson); } + + private static string NormalizeJson(this string json) => json.Replace("'", @"\u0027").Replace("\"", @"\u0022"); } diff --git a/tests/Samples.Server.Tests/StringExtensions.cs b/tests/Samples.Server.Tests/StringExtensions.cs index 70e29124..d0c15486 100644 --- a/tests/Samples.Server.Tests/StringExtensions.cs +++ b/tests/Samples.Server.Tests/StringExtensions.cs @@ -1,10 +1,9 @@ using GraphQL; -namespace Samples.Server.Tests +namespace Samples.Server.Tests; + +internal static class StringExtensions { - internal static class StringExtensions - { - public static Inputs ToInputs(this string value) - => new GraphQL.SystemTextJson.GraphQLSerializer().Deserialize(value); - } + public static Inputs ToInputs(this string value) + => new GraphQL.SystemTextJson.GraphQLSerializer().Deserialize(value); } diff --git a/tests/Transports.AspNetCore.Tests/NewtonsoftJsonTests.cs b/tests/Transports.AspNetCore.Tests/NewtonsoftJsonTests.cs index b3160e28..13e7aa7c 100644 --- a/tests/Transports.AspNetCore.Tests/NewtonsoftJsonTests.cs +++ b/tests/Transports.AspNetCore.Tests/NewtonsoftJsonTests.cs @@ -2,94 +2,93 @@ using System.Text; using GraphQL.Transport; -namespace GraphQL.Server.Transports.AspNetCore.Tests +namespace GraphQL.Server.Transports.AspNetCore.Tests; + +public partial class NewtonsoftJsonTests { - public partial class NewtonsoftJsonTests + [Fact] + public async Task Decodes_Request() { - [Fact] - public async Task Decodes_Request() - { - var request = @"{""query"":""abc"",""operationName"":""def"",""variables"":{""a"":""b"",""c"":2},""extensions"":{""d"":""e"",""f"":3}}"; - var ret = await Deserialize(request); - ret.Single().Query.ShouldBe("abc"); - ret.Single().OperationName.ShouldBe("def"); - ret.Single().Variables["a"].ShouldBe("b"); - ret.Single().Variables["c"].ShouldBe(2); - ret.Single().Extensions["d"].ShouldBe("e"); - ret.Single().Extensions["f"].ShouldBe(3); - } + var request = @"{""query"":""abc"",""operationName"":""def"",""variables"":{""a"":""b"",""c"":2},""extensions"":{""d"":""e"",""f"":3}}"; + var ret = await Deserialize(request); + ret.Single().Query.ShouldBe("abc"); + ret.Single().OperationName.ShouldBe("def"); + ret.Single().Variables["a"].ShouldBe("b"); + ret.Single().Variables["c"].ShouldBe(2); + ret.Single().Extensions["d"].ShouldBe("e"); + ret.Single().Extensions["f"].ShouldBe(3); + } - [Fact] - public async Task Decodes_Empty_Request() - { - var request = @"{}"; - var ret = await Deserialize(request); - ret.Single().Query.ShouldBeNull(); - ret.Single().OperationName.ShouldBeNull(); - ret.Single().Variables.ShouldBeNull(); - ret.Single().Extensions.ShouldBeNull(); - } + [Fact] + public async Task Decodes_Empty_Request() + { + var request = @"{}"; + var ret = await Deserialize(request); + ret.Single().Query.ShouldBeNull(); + ret.Single().OperationName.ShouldBeNull(); + ret.Single().Variables.ShouldBeNull(); + ret.Single().Extensions.ShouldBeNull(); + } - [Fact] - public async Task Decodes_BigInteger() - { - var request = @"{""variables"":{""a"":1234567890123456789012345678901234567890}}"; - var ret = await Deserialize(request); - var bi = BigInteger.Parse("1234567890123456789012345678901234567890"); - ret.Single().Variables["a"].ShouldBeOfType().ShouldBe(bi); - } + [Fact] + public async Task Decodes_BigInteger() + { + var request = @"{""variables"":{""a"":1234567890123456789012345678901234567890}}"; + var ret = await Deserialize(request); + var bi = BigInteger.Parse("1234567890123456789012345678901234567890"); + ret.Single().Variables["a"].ShouldBeOfType().ShouldBe(bi); + } - [Fact] - public async Task Dates_Should_Parse_As_Text() - { - var ret = await Deserialize(@"{""variables"":{""date"":""2015-12-22T10:10:10+03:00""}}"); - ret.Single().Variables["date"].ShouldBeOfType().ShouldBe("2015-12-22T10:10:10+03:00"); - } + [Fact] + public async Task Dates_Should_Parse_As_Text() + { + var ret = await Deserialize(@"{""variables"":{""date"":""2015-12-22T10:10:10+03:00""}}"); + ret.Single().Variables["date"].ShouldBeOfType().ShouldBe("2015-12-22T10:10:10+03:00"); + } - [Fact] - public async Task Extensions_Null_When_Not_Provided() - { - var ret = await Deserialize(@"{""variables"":{""date"":""2015-12-22T10:10:10+03:00""}}"); - ret.Single().Extensions.ShouldBeNull(); - } + [Fact] + public async Task Extensions_Null_When_Not_Provided() + { + var ret = await Deserialize(@"{""variables"":{""date"":""2015-12-22T10:10:10+03:00""}}"); + ret.Single().Extensions.ShouldBeNull(); + } - [Fact] - public async Task Name_Matching_Is_Case_Sensitive() - { - var ret = await Deserialize(@"{""VARIABLES"":{""date"":""2015-12-22T10:10:10+03:00""}}"); - ret.Single().Variables.ShouldBeNull(); - } + [Fact] + public async Task Name_Matching_Is_Case_Sensitive() + { + var ret = await Deserialize(@"{""VARIABLES"":{""date"":""2015-12-22T10:10:10+03:00""}}"); + ret.Single().Variables.ShouldBeNull(); + } - [Fact] - public async Task Decodes_Multiple_Queries() - { - var ret = await Deserialize(@"[{""query"":""abc""},{""query"":""def""}]"); - ret.Length.ShouldBe(2); - ret[0].Query.ShouldBe("abc"); - ret[1].Query.ShouldBe("def"); - } + [Fact] + public async Task Decodes_Multiple_Queries() + { + var ret = await Deserialize(@"[{""query"":""abc""},{""query"":""def""}]"); + ret.Length.ShouldBe(2); + ret[0].Query.ShouldBe("abc"); + ret[1].Query.ShouldBe("def"); + } - [Fact] - public async Task Decodes_Nested_Dictionaries() - { - var ret = await Deserialize(@"{""variables"":{""a"":{""b"":""c""}},""extensions"":{""d"":{""e"":""f""}}}"); - ret.Single().Variables["a"].ShouldBeOfType>()["b"].ShouldBe("c"); - ret.Single().Extensions["d"].ShouldBeOfType>()["e"].ShouldBe("f"); - } + [Fact] + public async Task Decodes_Nested_Dictionaries() + { + var ret = await Deserialize(@"{""variables"":{""a"":{""b"":""c""}},""extensions"":{""d"":{""e"":""f""}}}"); + ret.Single().Variables["a"].ShouldBeOfType>()["b"].ShouldBe("c"); + ret.Single().Extensions["d"].ShouldBeOfType>()["e"].ShouldBe("f"); + } - [Fact] - public async Task Decodes_Nested_Arrays() - { - var ret = await Deserialize(@"{""variables"":{""a"":[""b"",""c""]},""extensions"":{""d"":[""e"",""f""]}}"); - ret.Single().Variables["a"].ShouldBeOfType>().ShouldBe(new[] { "b", "c" }); - ret.Single().Extensions["d"].ShouldBeOfType>().ShouldBe(new[] { "e", "f" }); - } + [Fact] + public async Task Decodes_Nested_Arrays() + { + var ret = await Deserialize(@"{""variables"":{""a"":[""b"",""c""]},""extensions"":{""d"":[""e"",""f""]}}"); + ret.Single().Variables["a"].ShouldBeOfType>().ShouldBe(new[] { "b", "c" }); + ret.Single().Extensions["d"].ShouldBeOfType>().ShouldBe(new[] { "e", "f" }); + } - private async Task Deserialize(string jsonText) - { - var jsonStream = new System.IO.MemoryStream(Encoding.UTF8.GetBytes(jsonText)); - var deserializer = new NewtonsoftJson.GraphQLSerializer(); - return await deserializer.ReadAsync(jsonStream); - } + private async Task Deserialize(string jsonText) + { + var jsonStream = new System.IO.MemoryStream(Encoding.UTF8.GetBytes(jsonText)); + var deserializer = new NewtonsoftJson.GraphQLSerializer(); + return await deserializer.ReadAsync(jsonStream); } } diff --git a/tests/Transports.AspNetCore.Tests/SystemTextJsonTests.cs b/tests/Transports.AspNetCore.Tests/SystemTextJsonTests.cs index b2469657..8fa61df9 100644 --- a/tests/Transports.AspNetCore.Tests/SystemTextJsonTests.cs +++ b/tests/Transports.AspNetCore.Tests/SystemTextJsonTests.cs @@ -2,94 +2,93 @@ using System.Text; using GraphQL.Transport; -namespace GraphQL.Server.Transports.AspNetCore.Tests +namespace GraphQL.Server.Transports.AspNetCore.Tests; + +public partial class SystemTextJsonTests { - public partial class SystemTextJsonTests + [Fact] + public async Task Decodes_Request() { - [Fact] - public async Task Decodes_Request() - { - var request = @"{""query"":""abc"",""operationName"":""def"",""variables"":{""a"":""b"",""c"":2},""extensions"":{""d"":""e"",""f"":3}}"; - var ret = await Deserialize(request); - ret.Single().Query.ShouldBe("abc"); - ret.Single().OperationName.ShouldBe("def"); - ret.Single().Variables["a"].ShouldBe("b"); - ret.Single().Variables["c"].ShouldBe(2); - ret.Single().Extensions["d"].ShouldBe("e"); - ret.Single().Extensions["f"].ShouldBe(3); - } + var request = @"{""query"":""abc"",""operationName"":""def"",""variables"":{""a"":""b"",""c"":2},""extensions"":{""d"":""e"",""f"":3}}"; + var ret = await Deserialize(request); + ret.Single().Query.ShouldBe("abc"); + ret.Single().OperationName.ShouldBe("def"); + ret.Single().Variables["a"].ShouldBe("b"); + ret.Single().Variables["c"].ShouldBe(2); + ret.Single().Extensions["d"].ShouldBe("e"); + ret.Single().Extensions["f"].ShouldBe(3); + } - [Fact] - public async Task Decodes_Empty_Request() - { - var request = @"{}"; - var ret = await Deserialize(request); - ret.Single().Query.ShouldBeNull(); - ret.Single().OperationName.ShouldBeNull(); - ret.Single().Variables.ShouldBeNull(); - ret.Single().Extensions.ShouldBeNull(); - } + [Fact] + public async Task Decodes_Empty_Request() + { + var request = @"{}"; + var ret = await Deserialize(request); + ret.Single().Query.ShouldBeNull(); + ret.Single().OperationName.ShouldBeNull(); + ret.Single().Variables.ShouldBeNull(); + ret.Single().Extensions.ShouldBeNull(); + } - [Fact] - public async Task Decodes_BigInteger() - { - var request = @"{""variables"":{""a"":1234567890123456789012345678901234567890}}"; - var ret = await Deserialize(request); - var bi = BigInteger.Parse("1234567890123456789012345678901234567890"); - ret.Single().Variables["a"].ShouldBeOfType().ShouldBe(bi); - } + [Fact] + public async Task Decodes_BigInteger() + { + var request = @"{""variables"":{""a"":1234567890123456789012345678901234567890}}"; + var ret = await Deserialize(request); + var bi = BigInteger.Parse("1234567890123456789012345678901234567890"); + ret.Single().Variables["a"].ShouldBeOfType().ShouldBe(bi); + } - [Fact] - public async Task Dates_Should_Parse_As_Text() - { - var ret = await Deserialize(@"{""variables"":{""date"":""2015-12-22T10:10:10+03:00""}}"); - ret.Single().Variables["date"].ShouldBeOfType().ShouldBe("2015-12-22T10:10:10+03:00"); - } + [Fact] + public async Task Dates_Should_Parse_As_Text() + { + var ret = await Deserialize(@"{""variables"":{""date"":""2015-12-22T10:10:10+03:00""}}"); + ret.Single().Variables["date"].ShouldBeOfType().ShouldBe("2015-12-22T10:10:10+03:00"); + } - [Fact] - public async Task Extensions_Null_When_Not_Provided() - { - var ret = await Deserialize(@"{""variables"":{""date"":""2015-12-22T10:10:10+03:00""}}"); - ret.Single().Extensions.ShouldBeNull(); - } + [Fact] + public async Task Extensions_Null_When_Not_Provided() + { + var ret = await Deserialize(@"{""variables"":{""date"":""2015-12-22T10:10:10+03:00""}}"); + ret.Single().Extensions.ShouldBeNull(); + } - [Fact] - public async Task Name_Matching_Is_Case_Sensitive() - { - var ret = await Deserialize(@"{""VARIABLES"":{""date"":""2015-12-22T10:10:10+03:00""}}"); - ret.Single().Variables.ShouldBeNull(); - } + [Fact] + public async Task Name_Matching_Is_Case_Sensitive() + { + var ret = await Deserialize(@"{""VARIABLES"":{""date"":""2015-12-22T10:10:10+03:00""}}"); + ret.Single().Variables.ShouldBeNull(); + } - [Fact] - public async Task Decodes_Multiple_Queries() - { - var ret = await Deserialize(@"[{""query"":""abc""},{""query"":""def""}]"); - ret.Length.ShouldBe(2); - ret[0].Query.ShouldBe("abc"); - ret[1].Query.ShouldBe("def"); - } + [Fact] + public async Task Decodes_Multiple_Queries() + { + var ret = await Deserialize(@"[{""query"":""abc""},{""query"":""def""}]"); + ret.Length.ShouldBe(2); + ret[0].Query.ShouldBe("abc"); + ret[1].Query.ShouldBe("def"); + } - [Fact] - public async Task Decodes_Nested_Dictionaries() - { - var ret = await Deserialize(@"{""variables"":{""a"":{""b"":""c""}},""extensions"":{""d"":{""e"":""f""}}}"); - ret.Single().Variables["a"].ShouldBeOfType>()["b"].ShouldBe("c"); - ret.Single().Extensions["d"].ShouldBeOfType>()["e"].ShouldBe("f"); - } + [Fact] + public async Task Decodes_Nested_Dictionaries() + { + var ret = await Deserialize(@"{""variables"":{""a"":{""b"":""c""}},""extensions"":{""d"":{""e"":""f""}}}"); + ret.Single().Variables["a"].ShouldBeOfType>()["b"].ShouldBe("c"); + ret.Single().Extensions["d"].ShouldBeOfType>()["e"].ShouldBe("f"); + } - [Fact] - public async Task Decodes_Nested_Arrays() - { - var ret = await Deserialize(@"{""variables"":{""a"":[""b"",""c""]},""extensions"":{""d"":[""e"",""f""]}}"); - ret.Single().Variables["a"].ShouldBeOfType>().ShouldBe(new[] { "b", "c" }); - ret.Single().Extensions["d"].ShouldBeOfType>().ShouldBe(new[] { "e", "f" }); - } + [Fact] + public async Task Decodes_Nested_Arrays() + { + var ret = await Deserialize(@"{""variables"":{""a"":[""b"",""c""]},""extensions"":{""d"":[""e"",""f""]}}"); + ret.Single().Variables["a"].ShouldBeOfType>().ShouldBe(new[] { "b", "c" }); + ret.Single().Extensions["d"].ShouldBeOfType>().ShouldBe(new[] { "e", "f" }); + } - private async Task Deserialize(string jsonText) - { - var jsonStream = new System.IO.MemoryStream(Encoding.UTF8.GetBytes(jsonText)); - var deserializer = new SystemTextJson.GraphQLSerializer(_ => { }); - return await deserializer.ReadAsync(jsonStream); - } + private async Task Deserialize(string jsonText) + { + var jsonStream = new System.IO.MemoryStream(Encoding.UTF8.GetBytes(jsonText)); + var deserializer = new SystemTextJson.GraphQLSerializer(_ => { }); + return await deserializer.ReadAsync(jsonStream); } } diff --git a/tests/Transports.AspNetCore.Tests/TestHttpRequest.cs b/tests/Transports.AspNetCore.Tests/TestHttpRequest.cs index 0be6e238..e5e03e33 100644 --- a/tests/Transports.AspNetCore.Tests/TestHttpRequest.cs +++ b/tests/Transports.AspNetCore.Tests/TestHttpRequest.cs @@ -1,34 +1,33 @@ using System.IO.Pipelines; using Microsoft.AspNetCore.Http; -namespace GraphQL.Server.Transports.AspNetCore.Tests +namespace GraphQL.Server.Transports.AspNetCore.Tests; + +public class TestHttpRequest : HttpRequest { - public class TestHttpRequest : HttpRequest - { - public override HttpContext HttpContext => throw new NotImplementedException(); - - public override string Method { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } - public override string Scheme { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } - public override bool IsHttps { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } - public override HostString Host { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } - public override PathString PathBase { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } - public override PathString Path { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } - public override QueryString QueryString { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } - public override IQueryCollection Query { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } - public override string Protocol { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } - - public override IHeaderDictionary Headers => throw new NotImplementedException(); - - public override IRequestCookieCollection Cookies { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } - public override long? ContentLength { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } - public override string ContentType { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } - public override System.IO.Stream Body { get; set; } - public override PipeReader BodyReader => PipeReader.Create(Body); - - public override bool HasFormContentType => throw new NotImplementedException(); - - public override IFormCollection Form { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } - - public override Task ReadFormAsync(CancellationToken cancellationToken = default) => throw new NotImplementedException(); - } + public override HttpContext HttpContext => throw new NotImplementedException(); + + public override string Method { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } + public override string Scheme { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } + public override bool IsHttps { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } + public override HostString Host { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } + public override PathString PathBase { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } + public override PathString Path { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } + public override QueryString QueryString { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } + public override IQueryCollection Query { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } + public override string Protocol { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } + + public override IHeaderDictionary Headers => throw new NotImplementedException(); + + public override IRequestCookieCollection Cookies { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } + public override long? ContentLength { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } + public override string ContentType { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } + public override System.IO.Stream Body { get; set; } + public override PipeReader BodyReader => PipeReader.Create(Body); + + public override bool HasFormContentType => throw new NotImplementedException(); + + public override IFormCollection Form { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } + + public override Task ReadFormAsync(CancellationToken cancellationToken = default) => throw new NotImplementedException(); } diff --git a/tests/Transports.Subscriptions.Abstractions.Tests/NoopServiceScopeFactory.cs b/tests/Transports.Subscriptions.Abstractions.Tests/NoopServiceScopeFactory.cs index 040b18f0..bf7808dd 100644 --- a/tests/Transports.Subscriptions.Abstractions.Tests/NoopServiceScopeFactory.cs +++ b/tests/Transports.Subscriptions.Abstractions.Tests/NoopServiceScopeFactory.cs @@ -1,13 +1,12 @@ using Microsoft.Extensions.DependencyInjection; -namespace GraphQL.Server.Transports.Subscriptions.Abstractions.Tests +namespace GraphQL.Server.Transports.Subscriptions.Abstractions.Tests; + +internal sealed class NoopServiceScopeFactory : IServiceScopeFactory, IServiceScope { - internal sealed class NoopServiceScopeFactory : IServiceScopeFactory, IServiceScope - { - public static IServiceScopeFactory Instance { get; } = new NoopServiceScopeFactory(); - private NoopServiceScopeFactory() { } - IServiceScope IServiceScopeFactory.CreateScope() => this; - IServiceProvider IServiceScope.ServiceProvider => null; - void IDisposable.Dispose() { } - } + public static IServiceScopeFactory Instance { get; } = new NoopServiceScopeFactory(); + private NoopServiceScopeFactory() { } + IServiceScope IServiceScopeFactory.CreateScope() => this; + IServiceProvider IServiceScope.ServiceProvider => null; + void IDisposable.Dispose() { } } diff --git a/tests/Transports.Subscriptions.Abstractions.Tests/OperationMessageCollectorListener.cs b/tests/Transports.Subscriptions.Abstractions.Tests/OperationMessageCollectorListener.cs index 3faf6e5b..fb5699d6 100644 --- a/tests/Transports.Subscriptions.Abstractions.Tests/OperationMessageCollectorListener.cs +++ b/tests/Transports.Subscriptions.Abstractions.Tests/OperationMessageCollectorListener.cs @@ -1,26 +1,25 @@ using System.Collections.Concurrent; using GraphQL.Transport; -namespace GraphQL.Server.Transports.Subscriptions.Abstractions.Tests +namespace GraphQL.Server.Transports.Subscriptions.Abstractions.Tests; + +public class OperationMessageCollectorListener : IOperationMessageListener { - public class OperationMessageCollectorListener : IOperationMessageListener - { - public ConcurrentBag HandleMessages { get; } = new ConcurrentBag(); + public ConcurrentBag HandleMessages { get; } = new ConcurrentBag(); - public ConcurrentBag HandledMessages { get; } = new ConcurrentBag(); + public ConcurrentBag HandledMessages { get; } = new ConcurrentBag(); - public Task BeforeHandleAsync(MessageHandlingContext context) => Task.CompletedTask; + public Task BeforeHandleAsync(MessageHandlingContext context) => Task.CompletedTask; - public Task HandleAsync(MessageHandlingContext context) - { - HandleMessages.Add(context.Message); - return Task.CompletedTask; - } + public Task HandleAsync(MessageHandlingContext context) + { + HandleMessages.Add(context.Message); + return Task.CompletedTask; + } - public Task AfterHandleAsync(MessageHandlingContext context) - { - HandledMessages.Add(context.Message); - return Task.CompletedTask; - } + public Task AfterHandleAsync(MessageHandlingContext context) + { + HandledMessages.Add(context.Message); + return Task.CompletedTask; } } diff --git a/tests/Transports.Subscriptions.Abstractions.Tests/ProtocolHandlerFacts.cs b/tests/Transports.Subscriptions.Abstractions.Tests/ProtocolHandlerFacts.cs index 2b7aa45b..2caf7796 100644 --- a/tests/Transports.Subscriptions.Abstractions.Tests/ProtocolHandlerFacts.cs +++ b/tests/Transports.Subscriptions.Abstractions.Tests/ProtocolHandlerFacts.cs @@ -4,73 +4,73 @@ using Newtonsoft.Json.Linq; using NSubstitute; -namespace GraphQL.Server.Transports.Subscriptions.Abstractions.Tests +namespace GraphQL.Server.Transports.Subscriptions.Abstractions.Tests; + +public class ProtocolHandlerFacts { - public class ProtocolHandlerFacts + private readonly TestableSubscriptionTransport _transport; + private readonly TestableReader _transportReader; + private readonly TestableWriter _transportWriter; + private readonly IDocumentExecuter _documentExecuter; + private readonly SubscriptionManager _subscriptionManager; + private readonly SubscriptionServer _server; + private readonly ProtocolMessageListener _sut; + + public ProtocolHandlerFacts() { - private readonly TestableSubscriptionTransport _transport; - private readonly TestableReader _transportReader; - private readonly TestableWriter _transportWriter; - private readonly IDocumentExecuter _documentExecuter; - private readonly SubscriptionManager _subscriptionManager; - private readonly SubscriptionServer _server; - private readonly ProtocolMessageListener _sut; - - public ProtocolHandlerFacts() - { - _transport = new TestableSubscriptionTransport(); - _transportReader = _transport.Reader as TestableReader; - _transportWriter = _transport.Writer as TestableWriter; - _documentExecuter = Substitute.For(); - _documentExecuter.ExecuteAsync(null).ReturnsForAnyArgs( - new ExecutionResult + _transport = new TestableSubscriptionTransport(); + _transportReader = _transport.Reader as TestableReader; + _transportWriter = _transport.Writer as TestableWriter; + _documentExecuter = Substitute.For(); + _documentExecuter.ExecuteAsync(null).ReturnsForAnyArgs( + new ExecutionResult + { + Streams = new Dictionary> { - Streams = new Dictionary> - { - { "1", Substitute.For>() } - } - }); - _subscriptionManager = new SubscriptionManager(_documentExecuter, new NullLoggerFactory(), NoopServiceScopeFactory.Instance); - _sut = new ProtocolMessageListener(new NullLogger(), new GraphQLSerializer()); - _server = new SubscriptionServer( - _transport, - _subscriptionManager, - new[] { _sut }, - new NullLogger()); - } - - [Fact] - public async Task Receive_init() + { "1", Substitute.For>() } + } + }); + _subscriptionManager = new SubscriptionManager(_documentExecuter, new NullLoggerFactory(), NoopServiceScopeFactory.Instance); + _sut = new ProtocolMessageListener(new NullLogger(), new GraphQLSerializer()); + _server = new SubscriptionServer( + _transport, + _subscriptionManager, + new[] { _sut }, + new NullLogger()); + } + + [Fact] + public async Task Receive_init() + { + /* Given */ + var expected = new OperationMessage { - /* Given */ - var expected = new OperationMessage - { - Type = MessageType.GQL_CONNECTION_INIT - }; - _transportReader.AddMessageToRead(expected); - await _transportReader.Complete(); + Type = MessageType.GQL_CONNECTION_INIT + }; + _transportReader.AddMessageToRead(expected); + await _transportReader.Complete(); - /* When */ - await _server.OnConnect(); + /* When */ + await _server.OnConnect(); - /* Then */ - Assert.Contains(_transportWriter.WrittenMessages, - message => message.Type == MessageType.GQL_CONNECTION_ACK); - } + /* Then */ + Assert.Contains(_transportWriter.WrittenMessages, + message => message.Type == MessageType.GQL_CONNECTION_ACK); + } - [Fact] - public async Task Receive_start_mutation() + [Fact] + public async Task Receive_start_mutation() + { + /* Given */ + _documentExecuter.ExecuteAsync(null).ReturnsForAnyArgs( + new ExecutionResult()); + var expected = new OperationMessage { - /* Given */ - _documentExecuter.ExecuteAsync(null).ReturnsForAnyArgs( - new ExecutionResult()); - var expected = new OperationMessage + Type = MessageType.GQL_START, + Id = "1", + Payload = FromObject(new GraphQLRequest { - Type = MessageType.GQL_START, - Id = "1", - Payload = FromObject(new GraphQLRequest - { - Query = @"mutation AddMessage($message: MessageInputType!) { + Query = @"mutation AddMessage($message: MessageInputType!) { addMessage(message: $message) { from { id @@ -79,161 +79,160 @@ public async Task Receive_start_mutation() content } }" - }) - }; - _transportReader.AddMessageToRead(expected); - await _transportReader.Complete(); - - /* When */ - await _server.OnConnect(); - - /* Then */ - Assert.Empty(_server.Subscriptions); - Assert.Contains(_transportWriter.WrittenMessages, - message => message.Type == MessageType.GQL_DATA); - Assert.Contains(_transportWriter.WrittenMessages, - message => message.Type == MessageType.GQL_COMPLETE); - } - - [Fact] - public async Task Receive_start_query() + }) + }; + _transportReader.AddMessageToRead(expected); + await _transportReader.Complete(); + + /* When */ + await _server.OnConnect(); + + /* Then */ + Assert.Empty(_server.Subscriptions); + Assert.Contains(_transportWriter.WrittenMessages, + message => message.Type == MessageType.GQL_DATA); + Assert.Contains(_transportWriter.WrittenMessages, + message => message.Type == MessageType.GQL_COMPLETE); + } + + [Fact] + public async Task Receive_start_query() + { + /* Given */ + _documentExecuter.ExecuteAsync(null).ReturnsForAnyArgs( + new ExecutionResult()); + var expected = new OperationMessage { - /* Given */ - _documentExecuter.ExecuteAsync(null).ReturnsForAnyArgs( - new ExecutionResult()); - var expected = new OperationMessage + Type = MessageType.GQL_START, + Id = "1", + Payload = FromObject(new GraphQLRequest { - Type = MessageType.GQL_START, - Id = "1", - Payload = FromObject(new GraphQLRequest - { - Query = @"{ + Query = @"{ human() { name height } }" - }) - }; - _transportReader.AddMessageToRead(expected); - await _transportReader.Complete(); - - /* When */ - await _server.OnConnect(); - - /* Then */ - Assert.Empty(_server.Subscriptions); - Assert.Contains(_transportWriter.WrittenMessages, - message => message.Type == MessageType.GQL_DATA); - Assert.Contains(_transportWriter.WrittenMessages, - message => message.Type == MessageType.GQL_COMPLETE); - } - - [Fact] - public async Task Receive_start_subscription() + }) + }; + _transportReader.AddMessageToRead(expected); + await _transportReader.Complete(); + + /* When */ + await _server.OnConnect(); + + /* Then */ + Assert.Empty(_server.Subscriptions); + Assert.Contains(_transportWriter.WrittenMessages, + message => message.Type == MessageType.GQL_DATA); + Assert.Contains(_transportWriter.WrittenMessages, + message => message.Type == MessageType.GQL_COMPLETE); + } + + [Fact] + public async Task Receive_start_subscription() + { + /* Given */ + var expected = new OperationMessage { - /* Given */ - var expected = new OperationMessage + Type = MessageType.GQL_START, + Id = "1", + Payload = FromObject(new GraphQLRequest { - Type = MessageType.GQL_START, - Id = "1", - Payload = FromObject(new GraphQLRequest - { - Query = @"subscription MessageAdded { + Query = @"subscription MessageAdded { messageAdded { from { id displayName } content } }" - }) - }; - _transportReader.AddMessageToRead(expected); - await _transportReader.Complete(); + }) + }; + _transportReader.AddMessageToRead(expected); + await _transportReader.Complete(); - /* When */ - await _server.OnConnect(); + /* When */ + await _server.OnConnect(); - /* Then */ - Assert.Single(_server.Subscriptions, sub => sub.Id == expected.Id); - } + /* Then */ + Assert.Single(_server.Subscriptions, sub => sub.Id == expected.Id); + } - [Fact] - public async Task Receive_stop() + [Fact] + public async Task Receive_stop() + { + /* Given */ + var subscribe = new OperationMessage { - /* Given */ - var subscribe = new OperationMessage + Type = MessageType.GQL_START, + Id = "1", + Payload = FromObject(new GraphQLRequest { - Type = MessageType.GQL_START, - Id = "1", - Payload = FromObject(new GraphQLRequest - { - Query = "query" - }) - }; - _transportReader.AddMessageToRead(subscribe); + Query = "query" + }) + }; + _transportReader.AddMessageToRead(subscribe); - var unsubscribe = new OperationMessage - { - Type = MessageType.GQL_STOP, - Id = "1" - }; - _transportReader.AddMessageToRead(unsubscribe); - await _transportReader.Complete(); + var unsubscribe = new OperationMessage + { + Type = MessageType.GQL_STOP, + Id = "1" + }; + _transportReader.AddMessageToRead(unsubscribe); + await _transportReader.Complete(); - /* When */ - await _server.OnConnect(); + /* When */ + await _server.OnConnect(); - /* Then */ - Assert.Empty(_server.Subscriptions); - } + /* Then */ + Assert.Empty(_server.Subscriptions); + } - [Fact] - public async Task Receive_terminate() + [Fact] + public async Task Receive_terminate() + { + /* Given */ + var subscribe = new OperationMessage { - /* Given */ - var subscribe = new OperationMessage + Type = MessageType.GQL_CONNECTION_TERMINATE, + Id = "1", + Payload = FromObject(new GraphQLRequest { - Type = MessageType.GQL_CONNECTION_TERMINATE, - Id = "1", - Payload = FromObject(new GraphQLRequest - { - Query = "query" - }) - }; - _transportReader.AddMessageToRead(subscribe); - await _transportReader.Complete(); + Query = "query" + }) + }; + _transportReader.AddMessageToRead(subscribe); + await _transportReader.Complete(); - /* When */ - await _server.OnConnect(); + /* When */ + await _server.OnConnect(); - /* Then */ - Assert.Empty(_server.Subscriptions); - } + /* Then */ + Assert.Empty(_server.Subscriptions); + } - [Fact] - public async Task Receive_unknown() + [Fact] + public async Task Receive_unknown() + { + /* Given */ + var expected = new OperationMessage { - /* Given */ - var expected = new OperationMessage - { - Type = "x" - }; - _transportReader.AddMessageToRead(expected); - await _transportReader.Complete(); + Type = "x" + }; + _transportReader.AddMessageToRead(expected); + await _transportReader.Complete(); - /* When */ - await _server.OnConnect(); + /* When */ + await _server.OnConnect(); - /* Then */ - Assert.Contains(_transportWriter.WrittenMessages, - message => message.Type == MessageType.GQL_CONNECTION_ERROR); - } + /* Then */ + Assert.Contains(_transportWriter.WrittenMessages, + message => message.Type == MessageType.GQL_CONNECTION_ERROR); + } - private JObject FromObject(object value) - { - var serializer = new GraphQLSerializer(); - var data = serializer.Serialize(value); - return serializer.Deserialize(data); - } + private JObject FromObject(object value) + { + var serializer = new GraphQLSerializer(); + var data = serializer.Serialize(value); + return serializer.Deserialize(data); } } diff --git a/tests/Transports.Subscriptions.Abstractions.Tests/SchemaDocumentExecuter.cs b/tests/Transports.Subscriptions.Abstractions.Tests/SchemaDocumentExecuter.cs index 194c8ae1..983d90a8 100644 --- a/tests/Transports.Subscriptions.Abstractions.Tests/SchemaDocumentExecuter.cs +++ b/tests/Transports.Subscriptions.Abstractions.Tests/SchemaDocumentExecuter.cs @@ -1,19 +1,18 @@ using GraphQL.Types; -namespace GraphQL.Server.Transports.Subscriptions.Abstractions.Tests +namespace GraphQL.Server.Transports.Subscriptions.Abstractions.Tests; + +internal class SchemaDocumentExecuter : IDocumentExecuter { - internal class SchemaDocumentExecuter : IDocumentExecuter + private readonly ISchema _schema; + public SchemaDocumentExecuter(ISchema schema) { - private readonly ISchema _schema; - public SchemaDocumentExecuter(ISchema schema) - { - _schema = schema; - } + _schema = schema; + } - public Task ExecuteAsync(ExecutionOptions options) - { - options.Schema = _schema; - return new DocumentExecuter().ExecuteAsync(options); - } + public Task ExecuteAsync(ExecutionOptions options) + { + options.Schema = _schema; + return new DocumentExecuter().ExecuteAsync(options); } } diff --git a/tests/Transports.Subscriptions.Abstractions.Tests/Specs/ChatSpec.cs b/tests/Transports.Subscriptions.Abstractions.Tests/Specs/ChatSpec.cs index e35b5378..4d97aa8c 100644 --- a/tests/Transports.Subscriptions.Abstractions.Tests/Specs/ChatSpec.cs +++ b/tests/Transports.Subscriptions.Abstractions.Tests/Specs/ChatSpec.cs @@ -4,73 +4,73 @@ using Microsoft.Extensions.Logging.Abstractions; using Newtonsoft.Json.Linq; -namespace GraphQL.Server.Transports.Subscriptions.Abstractions.Tests.Specs +namespace GraphQL.Server.Transports.Subscriptions.Abstractions.Tests.Specs; + +public class ChatSpec { - public class ChatSpec + public ChatSpec() { - public ChatSpec() - { - _chat = new Chat(); - _transport = new TestableSubscriptionTransport(); - _transportReader = _transport.Reader as TestableReader; - _transportWriter = _transport.Writer as TestableWriter; - _subscriptions = new SubscriptionManager( - new SchemaDocumentExecuter(new ChatSchema(_chat, new DefaultServiceProvider())), - new NullLoggerFactory(), - NoopServiceScopeFactory.Instance); - - _server = new SubscriptionServer( - _transport, - _subscriptions, - new[] { new ProtocolMessageListener(new NullLogger(), new GraphQLSerializer()) }, - new NullLogger() - ); - } + _chat = new Chat(); + _transport = new TestableSubscriptionTransport(); + _transportReader = _transport.Reader as TestableReader; + _transportWriter = _transport.Writer as TestableWriter; + _subscriptions = new SubscriptionManager( + new SchemaDocumentExecuter(new ChatSchema(_chat, new DefaultServiceProvider())), + new NullLoggerFactory(), + NoopServiceScopeFactory.Instance); + + _server = new SubscriptionServer( + _transport, + _subscriptions, + new[] { new ProtocolMessageListener(new NullLogger(), new GraphQLSerializer()) }, + new NullLogger() + ); + } - private readonly Chat _chat; - private readonly TestableSubscriptionTransport _transport; - private readonly SubscriptionManager _subscriptions; - private readonly SubscriptionServer _server; - private readonly TestableReader _transportReader; - private readonly TestableWriter _transportWriter; + private readonly Chat _chat; + private readonly TestableSubscriptionTransport _transport; + private readonly SubscriptionManager _subscriptions; + private readonly SubscriptionServer _server; + private readonly TestableReader _transportReader; + private readonly TestableWriter _transportWriter; - private void AssertReceivedData(List writtenMessages, Predicate predicate) + private void AssertReceivedData(List writtenMessages, Predicate predicate) + { + var dataMessages = writtenMessages.Where(m => m.Type == MessageType.GQL_DATA); + var results = dataMessages.Select(m => { - var dataMessages = writtenMessages.Where(m => m.Type == MessageType.GQL_DATA); - var results = dataMessages.Select(m => - { - var executionResult = (ExecutionResult)m.Payload; - return FromObject(executionResult); - }).ToList(); + var executionResult = (ExecutionResult)m.Payload; + return FromObject(executionResult); + }).ToList(); - Assert.Contains(results, predicate); - } + Assert.Contains(results, predicate); + } - [Fact] - public async Task Mutate_messages() + [Fact] + public async Task Mutate_messages() + { + /* Given */ + _chat.AddMessage(new ReceivedMessage { - /* Given */ - _chat.AddMessage(new ReceivedMessage - { - Content = "test", - FromId = "1", - SentAt = DateTime.Now.Date - }); + Content = "test", + FromId = "1", + SentAt = DateTime.Now.Date + }); - string id = "1"; - _transportReader.AddMessageToRead(new OperationMessage - { - Id = id, - Type = MessageType.GQL_CONNECTION_INIT - }); - _transportReader.AddMessageToRead(new OperationMessage + string id = "1"; + _transportReader.AddMessageToRead(new OperationMessage + { + Id = id, + Type = MessageType.GQL_CONNECTION_INIT + }); + _transportReader.AddMessageToRead(new OperationMessage + { + Id = id, + Type = MessageType.GQL_START, + Payload = FromObject(new GraphQLRequest { - Id = id, - Type = MessageType.GQL_START, - Payload = FromObject(new GraphQLRequest - { - OperationName = "", - Query = @"mutation AddMessage($message: MessageInputType!) { + OperationName = "", + Query = @"mutation AddMessage($message: MessageInputType!) { addMessage(message: $message) { from { id @@ -79,54 +79,54 @@ public async Task Mutate_messages() content } }", - Variables = ToInputs(@"{ + Variables = ToInputs(@"{ ""message"": { ""content"": ""Message"", ""fromId"": ""1"" } }") - }) - }); - _transportReader.AddMessageToRead(new OperationMessage - { - Id = id, - Type = MessageType.GQL_CONNECTION_TERMINATE - }); + }) + }); + _transportReader.AddMessageToRead(new OperationMessage + { + Id = id, + Type = MessageType.GQL_CONNECTION_TERMINATE + }); - /* When */ - await _server.OnConnect(); + /* When */ + await _server.OnConnect(); - /* Then */ - Assert.Contains(_transportWriter.WrittenMessages, message => message.Type == MessageType.GQL_CONNECTION_ACK); - Assert.Contains(_transportWriter.WrittenMessages, message => message.Type == MessageType.GQL_DATA); - Assert.Contains(_transportWriter.WrittenMessages, message => message.Type == MessageType.GQL_COMPLETE); - } + /* Then */ + Assert.Contains(_transportWriter.WrittenMessages, message => message.Type == MessageType.GQL_CONNECTION_ACK); + Assert.Contains(_transportWriter.WrittenMessages, message => message.Type == MessageType.GQL_DATA); + Assert.Contains(_transportWriter.WrittenMessages, message => message.Type == MessageType.GQL_COMPLETE); + } - [Fact] - public async Task Query_messages() + [Fact] + public async Task Query_messages() + { + /* Given */ + _chat.AddMessage(new ReceivedMessage { - /* Given */ - _chat.AddMessage(new ReceivedMessage - { - Content = "test", - FromId = "1", - SentAt = DateTime.Now.Date - }); + Content = "test", + FromId = "1", + SentAt = DateTime.Now.Date + }); - string id = "1"; - _transportReader.AddMessageToRead(new OperationMessage - { - Id = id, - Type = MessageType.GQL_CONNECTION_INIT - }); - _transportReader.AddMessageToRead(new OperationMessage + string id = "1"; + _transportReader.AddMessageToRead(new OperationMessage + { + Id = id, + Type = MessageType.GQL_CONNECTION_INIT + }); + _transportReader.AddMessageToRead(new OperationMessage + { + Id = id, + Type = MessageType.GQL_START, + Payload = FromObject(new GraphQLRequest { - Id = id, - Type = MessageType.GQL_START, - Payload = FromObject(new GraphQLRequest - { - OperationName = "", - Query = @"query AllMessages { + OperationName = "", + Query = @"query AllMessages { messages { content sentAt @@ -136,82 +136,81 @@ public async Task Query_messages() } } }" - }) - }); - _transportReader.AddMessageToRead(new OperationMessage - { - Id = id, - Type = MessageType.GQL_CONNECTION_TERMINATE - }); + }) + }); + _transportReader.AddMessageToRead(new OperationMessage + { + Id = id, + Type = MessageType.GQL_CONNECTION_TERMINATE + }); - /* When */ - await _server.OnConnect(); + /* When */ + await _server.OnConnect(); - /* Then */ - Assert.Contains(_transportWriter.WrittenMessages, message => message.Type == MessageType.GQL_CONNECTION_ACK); - Assert.Contains(_transportWriter.WrittenMessages, message => message.Type == MessageType.GQL_DATA); - Assert.Contains(_transportWriter.WrittenMessages, message => message.Type == MessageType.GQL_COMPLETE); - } + /* Then */ + Assert.Contains(_transportWriter.WrittenMessages, message => message.Type == MessageType.GQL_CONNECTION_ACK); + Assert.Contains(_transportWriter.WrittenMessages, message => message.Type == MessageType.GQL_DATA); + Assert.Contains(_transportWriter.WrittenMessages, message => message.Type == MessageType.GQL_COMPLETE); + } - [Fact] - public async Task Subscribe_and_mutate_messages() + [Fact] + public async Task Subscribe_and_mutate_messages() + { + /* Given */ + // subscribe + string id = "1"; + _transportReader.AddMessageToRead(new OperationMessage { - /* Given */ - // subscribe - string id = "1"; - _transportReader.AddMessageToRead(new OperationMessage - { - Id = id, - Type = MessageType.GQL_CONNECTION_INIT - }); - _transportReader.AddMessageToRead(new OperationMessage + Id = id, + Type = MessageType.GQL_CONNECTION_INIT + }); + _transportReader.AddMessageToRead(new OperationMessage + { + Id = id, + Type = MessageType.GQL_START, + Payload = FromObject(new GraphQLRequest { - Id = id, - Type = MessageType.GQL_START, - Payload = FromObject(new GraphQLRequest - { - OperationName = "", - Query = @"subscription MessageAdded { + OperationName = "", + Query = @"subscription MessageAdded { messageAdded { from { id displayName } content sentAt } }" - }) - }); + }) + }); - // post message - _chat.AddMessage(new ReceivedMessage - { - FromId = "1", - Content = "content", - SentAt = DateTime.Now.Date - }); + // post message + _chat.AddMessage(new ReceivedMessage + { + FromId = "1", + Content = "content", + SentAt = DateTime.Now.Date + }); - /* When */ - _transportReader.AddMessageToRead(new OperationMessage - { - Id = id, - Type = MessageType.GQL_CONNECTION_TERMINATE - }); + /* When */ + _transportReader.AddMessageToRead(new OperationMessage + { + Id = id, + Type = MessageType.GQL_CONNECTION_TERMINATE + }); - await _server.OnConnect(); + await _server.OnConnect(); - /* Then */ - Assert.Contains(_transportWriter.WrittenMessages, message => message.Type == MessageType.GQL_CONNECTION_ACK); - AssertReceivedData(_transportWriter.WrittenMessages, data => ((JObject)data["data"]).ContainsKey("messageAdded")); - Assert.Contains(_transportWriter.WrittenMessages, message => message.Type == MessageType.GQL_COMPLETE); - } + /* Then */ + Assert.Contains(_transportWriter.WrittenMessages, message => message.Type == MessageType.GQL_CONNECTION_ACK); + AssertReceivedData(_transportWriter.WrittenMessages, data => ((JObject)data["data"]).ContainsKey("messageAdded")); + Assert.Contains(_transportWriter.WrittenMessages, message => message.Type == MessageType.GQL_COMPLETE); + } - private Inputs ToInputs(string json) - => new GraphQLSerializer().Deserialize(json); + private Inputs ToInputs(string json) + => new GraphQLSerializer().Deserialize(json); - private JObject FromObject(object value) - { - var serializer = new GraphQLSerializer(); - var data = serializer.Serialize(value); - return serializer.Deserialize(data); - } + private JObject FromObject(object value) + { + var serializer = new GraphQLSerializer(); + var data = serializer.Serialize(value); + return serializer.Deserialize(data); } } diff --git a/tests/Transports.Subscriptions.Abstractions.Tests/SubscriptionFacts.cs b/tests/Transports.Subscriptions.Abstractions.Tests/SubscriptionFacts.cs index 34930fd1..cfb9cf91 100644 --- a/tests/Transports.Subscriptions.Abstractions.Tests/SubscriptionFacts.cs +++ b/tests/Transports.Subscriptions.Abstractions.Tests/SubscriptionFacts.cs @@ -3,167 +3,166 @@ using Microsoft.Extensions.Logging.Abstractions; using NSubstitute; -namespace GraphQL.Server.Transports.Subscriptions.Abstractions.Tests +namespace GraphQL.Server.Transports.Subscriptions.Abstractions.Tests; + +public class SubscriptionFacts { - public class SubscriptionFacts + public SubscriptionFacts() { - public SubscriptionFacts() - { - _writer = Substitute.For(); - } + _writer = Substitute.For(); + } - private readonly IWriterPipeline _writer; + private readonly IWriterPipeline _writer; - [Fact] - public void On_data_from_stream() + [Fact] + public void On_data_from_stream() + { + /* Given */ + string id = "1"; + var payload = new GraphQLRequest(); + var stream = new ReplaySubject(1); + var result = new ExecutionResult { - /* Given */ - string id = "1"; - var payload = new GraphQLRequest(); - var stream = new ReplaySubject(1); - var result = new ExecutionResult + Streams = new Dictionary> { - Streams = new Dictionary> - { - { "op", stream } - } - }; - var expected = new ExecutionResult(); - var sut = new Subscription(id, payload, result, _writer, null, new NullLogger()); - - /* When */ - stream.OnNext(expected); - - /* Then */ - _writer.Received().Post( - Arg.Is( - message => message.Id == id - && message.Type == MessageType.GQL_DATA)); - } - - [Fact] - public void On_stream_complete() + { "op", stream } + } + }; + var expected = new ExecutionResult(); + var sut = new Subscription(id, payload, result, _writer, null, new NullLogger()); + + /* When */ + stream.OnNext(expected); + + /* Then */ + _writer.Received().Post( + Arg.Is( + message => message.Id == id + && message.Type == MessageType.GQL_DATA)); + } + + [Fact] + public void On_stream_complete() + { + /* Given */ + string id = "1"; + var payload = new GraphQLRequest(); + var stream = new ReplaySubject(1); + var result = new ExecutionResult { - /* Given */ - string id = "1"; - var payload = new GraphQLRequest(); - var stream = new ReplaySubject(1); - var result = new ExecutionResult + Streams = new Dictionary> { - Streams = new Dictionary> - { - { "op", stream } - } - }; - - var completed = Substitute.For>(); - var sut = new Subscription(id, payload, result, _writer, completed, new NullLogger()); - - /* When */ - stream.OnCompleted(); - - /* Then */ - Assert.False(stream.HasObservers); - completed.Received().Invoke(sut); - _writer.Received().Post( - Arg.Is( - message => message.Id == id - && message.Type == MessageType.GQL_COMPLETE)); - } - - [Fact] - public void Subscribe_to_stream() + { "op", stream } + } + }; + + var completed = Substitute.For>(); + var sut = new Subscription(id, payload, result, _writer, completed, new NullLogger()); + + /* When */ + stream.OnCompleted(); + + /* Then */ + Assert.False(stream.HasObservers); + completed.Received().Invoke(sut); + _writer.Received().Post( + Arg.Is( + message => message.Id == id + && message.Type == MessageType.GQL_COMPLETE)); + } + + [Fact] + public void Subscribe_to_stream() + { + /* Given */ + string id = "1"; + var payload = new GraphQLRequest(); + var stream = new Subject(); + var result = new ExecutionResult { - /* Given */ - string id = "1"; - var payload = new GraphQLRequest(); - var stream = new Subject(); - var result = new ExecutionResult + Streams = new Dictionary> { - Streams = new Dictionary> - { - { "op", stream } - } - }; + { "op", stream } + } + }; - /* When */ - var sut = new Subscription(id, payload, result, _writer, null, new NullLogger()); + /* When */ + var sut = new Subscription(id, payload, result, _writer, null, new NullLogger()); - /* Then */ - Assert.True(stream.HasObservers); - } + /* Then */ + Assert.True(stream.HasObservers); + } - [Fact] - public void Subscribe_to_completed_stream_should_not_throw() + [Fact] + public void Subscribe_to_completed_stream_should_not_throw() + { + /* Given */ + string id = "1"; + var payload = new GraphQLRequest(); + var subject = new Subject(); + subject.OnCompleted(); + var stream = subject; + var result = new ExecutionResult { - /* Given */ - string id = "1"; - var payload = new GraphQLRequest(); - var subject = new Subject(); - subject.OnCompleted(); - var stream = subject; - var result = new ExecutionResult + Streams = new Dictionary> { - Streams = new Dictionary> - { - { "op", stream } - } - }; - - /* When */ - /* Then */ - var sut = new Subscription(id, payload, result, _writer, null, new NullLogger()); - } - - [Fact] - public async Task Unsubscribe_from_stream() + { "op", stream } + } + }; + + /* When */ + /* Then */ + var sut = new Subscription(id, payload, result, _writer, null, new NullLogger()); + } + + [Fact] + public async Task Unsubscribe_from_stream() + { + /* Given */ + string id = "1"; + var payload = new GraphQLRequest(); + var unsubscribe = Substitute.For(); + var stream = Substitute.For>(); + stream.Subscribe(null).ReturnsForAnyArgs(unsubscribe); + var result = new ExecutionResult { - /* Given */ - string id = "1"; - var payload = new GraphQLRequest(); - var unsubscribe = Substitute.For(); - var stream = Substitute.For>(); - stream.Subscribe(null).ReturnsForAnyArgs(unsubscribe); - var result = new ExecutionResult + Streams = new Dictionary> { - Streams = new Dictionary> - { - { "op", stream } - } - }; - var sut = new Subscription(id, payload, result, _writer, null, new NullLogger()); - - /* When */ - await sut.UnsubscribeAsync(); - - /* Then */ - unsubscribe.Received().Dispose(); - } - - [Fact] - public async Task Write_Complete_on_unsubscribe() + { "op", stream } + } + }; + var sut = new Subscription(id, payload, result, _writer, null, new NullLogger()); + + /* When */ + await sut.UnsubscribeAsync(); + + /* Then */ + unsubscribe.Received().Dispose(); + } + + [Fact] + public async Task Write_Complete_on_unsubscribe() + { + /* Given */ + string id = "1"; + var payload = new GraphQLRequest(); + var result = new ExecutionResult { - /* Given */ - string id = "1"; - var payload = new GraphQLRequest(); - var result = new ExecutionResult + Streams = new Dictionary> { - Streams = new Dictionary> - { - { "1", Substitute.For>() } - } - }; - - var sut = new Subscription(id, payload, result, _writer, null, new NullLogger()); - - /* When */ - await sut.UnsubscribeAsync(); - - /* Then */ - await _writer.Received().SendAsync( - Arg.Is( - message => message.Id == id - && message.Type == MessageType.GQL_COMPLETE)); - } + { "1", Substitute.For>() } + } + }; + + var sut = new Subscription(id, payload, result, _writer, null, new NullLogger()); + + /* When */ + await sut.UnsubscribeAsync(); + + /* Then */ + await _writer.Received().SendAsync( + Arg.Is( + message => message.Id == id + && message.Type == MessageType.GQL_COMPLETE)); } } diff --git a/tests/Transports.Subscriptions.Abstractions.Tests/SubscriptionManagerFacts.cs b/tests/Transports.Subscriptions.Abstractions.Tests/SubscriptionManagerFacts.cs index 4e3bc680..a6edac86 100644 --- a/tests/Transports.Subscriptions.Abstractions.Tests/SubscriptionManagerFacts.cs +++ b/tests/Transports.Subscriptions.Abstractions.Tests/SubscriptionManagerFacts.cs @@ -2,175 +2,174 @@ using Microsoft.Extensions.Logging.Abstractions; using NSubstitute; -namespace GraphQL.Server.Transports.Subscriptions.Abstractions.Tests +namespace GraphQL.Server.Transports.Subscriptions.Abstractions.Tests; + +public class SubscriptionManagerFacts { - public class SubscriptionManagerFacts + public SubscriptionManagerFacts() { - public SubscriptionManagerFacts() - { - _writer = Substitute.For(); - _executer = Substitute.For(); - _executer.ExecuteAsync(null).ReturnsForAnyArgs( - new ExecutionResult + _writer = Substitute.For(); + _executer = Substitute.For(); + _executer.ExecuteAsync(null).ReturnsForAnyArgs( + new ExecutionResult + { + Streams = new Dictionary> { - Streams = new Dictionary> - { - { "1", Substitute.For>() } - } - }); - _sut = new SubscriptionManager(_executer, new NullLoggerFactory(), NoopServiceScopeFactory.Instance); - _server = new TestableServerOperations(null, _writer, _sut); - } - - private readonly SubscriptionManager _sut; - private readonly IDocumentExecuter _executer; - private readonly IWriterPipeline _writer; - private readonly IServerOperations _server; - - [Fact] - public async Task Failed_Subscribe_does_not_add() - { - /* Given */ - string id = "1"; - var payload = new GraphQLRequest(); - var context = new MessageHandlingContext(_server, null); - - _executer.ExecuteAsync(null).ReturnsForAnyArgs( - new ExecutionResult + { "1", Substitute.For>() } + } + }); + _sut = new SubscriptionManager(_executer, new NullLoggerFactory(), NoopServiceScopeFactory.Instance); + _server = new TestableServerOperations(null, _writer, _sut); + } + + private readonly SubscriptionManager _sut; + private readonly IDocumentExecuter _executer; + private readonly IWriterPipeline _writer; + private readonly IServerOperations _server; + + [Fact] + public async Task Failed_Subscribe_does_not_add() + { + /* Given */ + string id = "1"; + var payload = new GraphQLRequest(); + var context = new MessageHandlingContext(_server, null); + + _executer.ExecuteAsync(null).ReturnsForAnyArgs( + new ExecutionResult + { + Errors = new ExecutionErrors { - Errors = new ExecutionErrors - { - new ExecutionError("error") - } - }); - - /* When */ - await _sut.SubscribeOrExecuteAsync(id, payload, context); - - /* Then */ - Assert.Empty(_sut); - } - - [Fact] - public async Task Failed_Subscribe_with_null_stream() - { - /* Given */ - string id = "1"; - var payload = new GraphQLRequest(); - var context = new MessageHandlingContext(_server, null); - - _executer.ExecuteAsync(null).ReturnsForAnyArgs( - new ExecutionResult + new ExecutionError("error") + } + }); + + /* When */ + await _sut.SubscribeOrExecuteAsync(id, payload, context); + + /* Then */ + Assert.Empty(_sut); + } + + [Fact] + public async Task Failed_Subscribe_with_null_stream() + { + /* Given */ + string id = "1"; + var payload = new GraphQLRequest(); + var context = new MessageHandlingContext(_server, null); + + _executer.ExecuteAsync(null).ReturnsForAnyArgs( + new ExecutionResult + { + Streams = new Dictionary> { - Streams = new Dictionary> - { - { "1", null } - } - }); - - /* When */ - await _sut.SubscribeOrExecuteAsync(id, payload, context); - - /* Then */ - await _writer.Received().SendAsync( - Arg.Is( - message => message.Id == id - && message.Type == MessageType.GQL_ERROR)); - } - - [Fact] - public async Task Failed_Subscribe_writes_error() - { - /* Given */ - string id = "1"; - var payload = new GraphQLRequest(); - var context = new MessageHandlingContext(_server, null); - - _executer.ExecuteAsync(null).ReturnsForAnyArgs( - new ExecutionResult + { "1", null } + } + }); + + /* When */ + await _sut.SubscribeOrExecuteAsync(id, payload, context); + + /* Then */ + await _writer.Received().SendAsync( + Arg.Is( + message => message.Id == id + && message.Type == MessageType.GQL_ERROR)); + } + + [Fact] + public async Task Failed_Subscribe_writes_error() + { + /* Given */ + string id = "1"; + var payload = new GraphQLRequest(); + var context = new MessageHandlingContext(_server, null); + + _executer.ExecuteAsync(null).ReturnsForAnyArgs( + new ExecutionResult + { + Errors = new ExecutionErrors { - Errors = new ExecutionErrors - { - new ExecutionError("error") - } - }); - - /* When */ - await _sut.SubscribeOrExecuteAsync(id, payload, context); - - /* Then */ - await _writer.Received().SendAsync( - Arg.Is( - message => message.Id == id - && message.Type == MessageType.GQL_ERROR)); - } - - [Fact] - public async Task Subscribe_adds() - { - /* Given */ - string id = "1"; - var payload = new GraphQLRequest(); - var context = new MessageHandlingContext(_server, null); - - /* When */ - await _sut.SubscribeOrExecuteAsync(id, payload, context); - - /* Then */ - Assert.Single(_sut, sub => sub.Id == id); - } - - [Fact] - public async Task Subscribe_executes() - { - /* Given */ - string id = "1"; - var payload = new GraphQLRequest(); - var context = new MessageHandlingContext(_server, null); - - /* When */ - await _sut.SubscribeOrExecuteAsync(id, payload, context); - - /* Then */ - await _executer.Received().ExecuteAsync( - Arg.Any()); - } - - [Fact] - public async Task Unsubscribe_removes() - { - /* Given */ - string id = "1"; - var payload = new GraphQLRequest(); - var context = new MessageHandlingContext(_server, null); - - await _sut.SubscribeOrExecuteAsync(id, payload, context); - - /* When */ - await _sut.UnsubscribeAsync(id); - - /* Then */ - Assert.Empty(_sut); - } - - [Fact] - public async Task Unsubscribe_writes_complete() - { - /* Given */ - string id = "1"; - var payload = new GraphQLRequest(); - var context = new MessageHandlingContext(_server, null); - - await _sut.SubscribeOrExecuteAsync(id, payload, context); - - /* When */ - await _sut.UnsubscribeAsync(id); - - /* Then */ - await _writer.Received().SendAsync( - Arg.Is( - message => message.Id == id - && message.Type == MessageType.GQL_COMPLETE)); - } + new ExecutionError("error") + } + }); + + /* When */ + await _sut.SubscribeOrExecuteAsync(id, payload, context); + + /* Then */ + await _writer.Received().SendAsync( + Arg.Is( + message => message.Id == id + && message.Type == MessageType.GQL_ERROR)); + } + + [Fact] + public async Task Subscribe_adds() + { + /* Given */ + string id = "1"; + var payload = new GraphQLRequest(); + var context = new MessageHandlingContext(_server, null); + + /* When */ + await _sut.SubscribeOrExecuteAsync(id, payload, context); + + /* Then */ + Assert.Single(_sut, sub => sub.Id == id); + } + + [Fact] + public async Task Subscribe_executes() + { + /* Given */ + string id = "1"; + var payload = new GraphQLRequest(); + var context = new MessageHandlingContext(_server, null); + + /* When */ + await _sut.SubscribeOrExecuteAsync(id, payload, context); + + /* Then */ + await _executer.Received().ExecuteAsync( + Arg.Any()); + } + + [Fact] + public async Task Unsubscribe_removes() + { + /* Given */ + string id = "1"; + var payload = new GraphQLRequest(); + var context = new MessageHandlingContext(_server, null); + + await _sut.SubscribeOrExecuteAsync(id, payload, context); + + /* When */ + await _sut.UnsubscribeAsync(id); + + /* Then */ + Assert.Empty(_sut); + } + + [Fact] + public async Task Unsubscribe_writes_complete() + { + /* Given */ + string id = "1"; + var payload = new GraphQLRequest(); + var context = new MessageHandlingContext(_server, null); + + await _sut.SubscribeOrExecuteAsync(id, payload, context); + + /* When */ + await _sut.UnsubscribeAsync(id); + + /* Then */ + await _writer.Received().SendAsync( + Arg.Is( + message => message.Id == id + && message.Type == MessageType.GQL_COMPLETE)); } } diff --git a/tests/Transports.Subscriptions.Abstractions.Tests/SubscriptionServerFacts.cs b/tests/Transports.Subscriptions.Abstractions.Tests/SubscriptionServerFacts.cs index 3a91f6a6..a2ca17ab 100644 --- a/tests/Transports.Subscriptions.Abstractions.Tests/SubscriptionServerFacts.cs +++ b/tests/Transports.Subscriptions.Abstractions.Tests/SubscriptionServerFacts.cs @@ -2,105 +2,104 @@ using Microsoft.Extensions.Logging.Abstractions; using NSubstitute; -namespace GraphQL.Server.Transports.Subscriptions.Abstractions.Tests +namespace GraphQL.Server.Transports.Subscriptions.Abstractions.Tests; + +public class SubscriptionServerFacts { - public class SubscriptionServerFacts + public SubscriptionServerFacts() { - public SubscriptionServerFacts() - { - _messageListener = Substitute.For(); - _transport = new TestableSubscriptionTransport(); - _transportReader = _transport.Reader as TestableReader; - _transportWriter = _transport.Writer as TestableWriter; - _documentExecuter = Substitute.For(); - _documentExecuter.ExecuteAsync(null).ReturnsForAnyArgs( - new ExecutionResult + _messageListener = Substitute.For(); + _transport = new TestableSubscriptionTransport(); + _transportReader = _transport.Reader as TestableReader; + _transportWriter = _transport.Writer as TestableWriter; + _documentExecuter = Substitute.For(); + _documentExecuter.ExecuteAsync(null).ReturnsForAnyArgs( + new ExecutionResult + { + Streams = new Dictionary> { - Streams = new Dictionary> - { - { "1", Substitute.For>() } - } - }); - _subscriptionManager = new SubscriptionManager(_documentExecuter, new NullLoggerFactory(), NoopServiceScopeFactory.Instance); - _sut = new SubscriptionServer( - _transport, - _subscriptionManager, - new[] { _messageListener }, - new NullLogger()); - } + { "1", Substitute.For>() } + } + }); + _subscriptionManager = new SubscriptionManager(_documentExecuter, new NullLoggerFactory(), NoopServiceScopeFactory.Instance); + _sut = new SubscriptionServer( + _transport, + _subscriptionManager, + new[] { _messageListener }, + new NullLogger()); + } - private readonly TestableSubscriptionTransport _transport; - private readonly SubscriptionServer _sut; - private readonly ISubscriptionManager _subscriptionManager; - private readonly IDocumentExecuter _documentExecuter; - private readonly IOperationMessageListener _messageListener; - private readonly TestableReader _transportReader; - private readonly TestableWriter _transportWriter; + private readonly TestableSubscriptionTransport _transport; + private readonly SubscriptionServer _sut; + private readonly ISubscriptionManager _subscriptionManager; + private readonly IDocumentExecuter _documentExecuter; + private readonly IOperationMessageListener _messageListener; + private readonly TestableReader _transportReader; + private readonly TestableWriter _transportWriter; - [Fact] - public async Task Listener_BeforeHandle() + [Fact] + public async Task Listener_BeforeHandle() + { + /* Given */ + var expected = new OperationMessage { - /* Given */ - var expected = new OperationMessage - { - Type = MessageType.GQL_CONNECTION_INIT - }; - _transportReader.AddMessageToRead(expected); - await _transportReader.Complete(); + Type = MessageType.GQL_CONNECTION_INIT + }; + _transportReader.AddMessageToRead(expected); + await _transportReader.Complete(); - /* When */ - await _sut.OnConnect(); + /* When */ + await _sut.OnConnect(); - /* Then */ - await _messageListener.Received().BeforeHandleAsync(Arg.Is(context => - context.Writer == _transportWriter - && context.Reader == _transportReader - && context.Subscriptions == _subscriptionManager - && context.Message == expected)); - } + /* Then */ + await _messageListener.Received().BeforeHandleAsync(Arg.Is(context => + context.Writer == _transportWriter + && context.Reader == _transportReader + && context.Subscriptions == _subscriptionManager + && context.Message == expected)); + } - [Fact] - public async Task Listener_Handle() + [Fact] + public async Task Listener_Handle() + { + /* Given */ + var expected = new OperationMessage { - /* Given */ - var expected = new OperationMessage - { - Type = MessageType.GQL_CONNECTION_INIT - }; - _transportReader.AddMessageToRead(expected); - await _transportReader.Complete(); + Type = MessageType.GQL_CONNECTION_INIT + }; + _transportReader.AddMessageToRead(expected); + await _transportReader.Complete(); - /* When */ - await _sut.OnConnect(); + /* When */ + await _sut.OnConnect(); - /* Then */ - await _messageListener.Received().HandleAsync(Arg.Is(context => - context.Writer == _transportWriter - && context.Reader == _transportReader - && context.Subscriptions == _subscriptionManager - && context.Message == expected)); - } + /* Then */ + await _messageListener.Received().HandleAsync(Arg.Is(context => + context.Writer == _transportWriter + && context.Reader == _transportReader + && context.Subscriptions == _subscriptionManager + && context.Message == expected)); + } - [Fact] - public async Task Listener_AfterHandle() + [Fact] + public async Task Listener_AfterHandle() + { + /* Given */ + var expected = new OperationMessage { - /* Given */ - var expected = new OperationMessage - { - Type = MessageType.GQL_CONNECTION_INIT - }; - _transportReader.AddMessageToRead(expected); - await _transportReader.Complete(); + Type = MessageType.GQL_CONNECTION_INIT + }; + _transportReader.AddMessageToRead(expected); + await _transportReader.Complete(); - /* When */ - await _sut.OnConnect(); + /* When */ + await _sut.OnConnect(); - /* Then */ - await _messageListener.Received().AfterHandleAsync(Arg.Is(context => - context.Writer == _transportWriter - && context.Reader == _transportReader - && context.Subscriptions == _subscriptionManager - && context.Message == expected)); - } + /* Then */ + await _messageListener.Received().AfterHandleAsync(Arg.Is(context => + context.Writer == _transportWriter + && context.Reader == _transportReader + && context.Subscriptions == _subscriptionManager + && context.Message == expected)); } } diff --git a/tests/Transports.Subscriptions.Abstractions.Tests/TestableServerOperations.cs b/tests/Transports.Subscriptions.Abstractions.Tests/TestableServerOperations.cs index 3928487f..e3b4c6d2 100644 --- a/tests/Transports.Subscriptions.Abstractions.Tests/TestableServerOperations.cs +++ b/tests/Transports.Subscriptions.Abstractions.Tests/TestableServerOperations.cs @@ -1,27 +1,26 @@ -namespace GraphQL.Server.Transports.Subscriptions.Abstractions.Tests +namespace GraphQL.Server.Transports.Subscriptions.Abstractions.Tests; + +public class TestableServerOperations : IServerOperations { - public class TestableServerOperations : IServerOperations + public TestableServerOperations( + IReaderPipeline reader, + IWriterPipeline writer, + ISubscriptionManager subscriptions) { - public TestableServerOperations( - IReaderPipeline reader, - IWriterPipeline writer, - ISubscriptionManager subscriptions) - { - TransportReader = reader; - TransportWriter = writer; - Subscriptions = subscriptions; - } + TransportReader = reader; + TransportWriter = writer; + Subscriptions = subscriptions; + } - public Task Terminate() - { - IsTerminated = true; - return Task.CompletedTask; - } + public Task Terminate() + { + IsTerminated = true; + return Task.CompletedTask; + } - public bool IsTerminated { get; set; } + public bool IsTerminated { get; set; } - public IReaderPipeline TransportReader { get; } - public IWriterPipeline TransportWriter { get; } - public ISubscriptionManager Subscriptions { get; } - } + public IReaderPipeline TransportReader { get; } + public IWriterPipeline TransportWriter { get; } + public ISubscriptionManager Subscriptions { get; } } diff --git a/tests/Transports.Subscriptions.Abstractions.Tests/TestableSubscriptionTransport.cs b/tests/Transports.Subscriptions.Abstractions.Tests/TestableSubscriptionTransport.cs index ce11b6f2..e291cfd1 100644 --- a/tests/Transports.Subscriptions.Abstractions.Tests/TestableSubscriptionTransport.cs +++ b/tests/Transports.Subscriptions.Abstractions.Tests/TestableSubscriptionTransport.cs @@ -1,71 +1,70 @@ using System.Threading.Tasks.Dataflow; using GraphQL.Transport; -namespace GraphQL.Server.Transports.Subscriptions.Abstractions.Tests +namespace GraphQL.Server.Transports.Subscriptions.Abstractions.Tests; + +public class TestableReader : IReaderPipeline { - public class TestableReader : IReaderPipeline + private readonly BufferBlock _readBuffer; + + public TestableReader() { - private readonly BufferBlock _readBuffer; + _readBuffer = new BufferBlock(); + } - public TestableReader() + public void LinkTo(ITargetBlock target) + { + _readBuffer.LinkTo(target, new DataflowLinkOptions { - _readBuffer = new BufferBlock(); - } + PropagateCompletion = true + }); + } - public void LinkTo(ITargetBlock target) - { - _readBuffer.LinkTo(target, new DataflowLinkOptions - { - PropagateCompletion = true - }); - } + public Task Complete() + { + _readBuffer.Complete(); + return Task.CompletedTask; + } - public Task Complete() - { - _readBuffer.Complete(); - return Task.CompletedTask; - } + public Task Completion => _readBuffer.Completion; - public Task Completion => _readBuffer.Completion; + public bool AddMessageToRead(OperationMessage message) => _readBuffer.Post(message); +} - public bool AddMessageToRead(OperationMessage message) => _readBuffer.Post(message); - } +public class TestableWriter : IWriterPipeline +{ + private readonly ActionBlock _endBlock; - public class TestableWriter : IWriterPipeline + public TestableWriter() { - private readonly ActionBlock _endBlock; - - public TestableWriter() - { - WrittenMessages = new List(); - _endBlock = new ActionBlock(message => WrittenMessages.Add(message)); - } + WrittenMessages = new List(); + _endBlock = new ActionBlock(message => WrittenMessages.Add(message)); + } - public List WrittenMessages { get; } + public List WrittenMessages { get; } - public bool Post(OperationMessage message) => _endBlock.Post(message); + public bool Post(OperationMessage message) => _endBlock.Post(message); - public Task SendAsync(OperationMessage message) => _endBlock.SendAsync(message); + public Task SendAsync(OperationMessage message) => _endBlock.SendAsync(message); - public Task Completion => _endBlock.Completion; + public Task Completion => _endBlock.Completion; - public Task Complete() - { - _endBlock.Complete(); - return Task.CompletedTask; - } + public Task Complete() + { + _endBlock.Complete(); + return Task.CompletedTask; } +} - public class TestableSubscriptionTransport : IMessageTransport +public class TestableSubscriptionTransport : IMessageTransport +{ + public TestableSubscriptionTransport() { - public TestableSubscriptionTransport() - { - Writer = new TestableWriter(); - Reader = new TestableReader(); - } + Writer = new TestableWriter(); + Reader = new TestableReader(); + } - public IReaderPipeline Reader { get; } + public IReaderPipeline Reader { get; } - public IWriterPipeline Writer { get; } - } + public IWriterPipeline Writer { get; } } diff --git a/tests/Transports.Subscriptions.WebSockets.Tests/TestMessage.cs b/tests/Transports.Subscriptions.WebSockets.Tests/TestMessage.cs index 474545ff..6dd2d772 100644 --- a/tests/Transports.Subscriptions.WebSockets.Tests/TestMessage.cs +++ b/tests/Transports.Subscriptions.WebSockets.Tests/TestMessage.cs @@ -1,9 +1,8 @@ -namespace GraphQL.Server.Transports.WebSockets.Tests +namespace GraphQL.Server.Transports.WebSockets.Tests; + +public class TestMessage { - public class TestMessage - { - public string Content { get; set; } + public string Content { get; set; } - public DateTimeOffset SentAt { get; set; } - } + public DateTimeOffset SentAt { get; set; } } diff --git a/tests/Transports.Subscriptions.WebSockets.Tests/TestSchema.cs b/tests/Transports.Subscriptions.WebSockets.Tests/TestSchema.cs index 5df820b0..1cdde3f6 100644 --- a/tests/Transports.Subscriptions.WebSockets.Tests/TestSchema.cs +++ b/tests/Transports.Subscriptions.WebSockets.Tests/TestSchema.cs @@ -1,8 +1,7 @@ using GraphQL.Types; -namespace GraphQL.Server.Transports.WebSockets.Tests +namespace GraphQL.Server.Transports.WebSockets.Tests; + +public class TestSchema : Schema { - public class TestSchema : Schema - { - } } diff --git a/tests/Transports.Subscriptions.WebSockets.Tests/TestStartup.cs b/tests/Transports.Subscriptions.WebSockets.Tests/TestStartup.cs index 0e77be5c..2d71d3dd 100644 --- a/tests/Transports.Subscriptions.WebSockets.Tests/TestStartup.cs +++ b/tests/Transports.Subscriptions.WebSockets.Tests/TestStartup.cs @@ -3,27 +3,26 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -namespace GraphQL.Server.Transports.WebSockets.Tests +namespace GraphQL.Server.Transports.WebSockets.Tests; + +public class TestStartup { - public class TestStartup + public void ConfigureServices(IServiceCollection services) { - public void ConfigureServices(IServiceCollection services) - { - services - .AddGraphQL(builder => builder.AddWebSockets().AddWebSocketsHttpMiddleware()) - .AddSingleton() - .AddLogging(builder => - { - // prevent writing errors to Console.Error during tests (required for testing on ubuntu) - builder.ClearProviders(); - builder.AddDebug(); - }); - } + services + .AddGraphQL(builder => builder.AddWebSockets().AddWebSocketsHttpMiddleware()) + .AddSingleton() + .AddLogging(builder => + { + // prevent writing errors to Console.Error during tests (required for testing on ubuntu) + builder.ClearProviders(); + builder.AddDebug(); + }); + } - public void Configure(IApplicationBuilder app) - { - app.UseWebSockets(); - app.UseGraphQLWebSockets("/graphql"); - } + public void Configure(IApplicationBuilder app) + { + app.UseWebSockets(); + app.UseGraphQLWebSockets("/graphql"); } } diff --git a/tests/Transports.Subscriptions.WebSockets.Tests/TestWebSocket.cs b/tests/Transports.Subscriptions.WebSockets.Tests/TestWebSocket.cs index a17e02b5..d4e085c1 100644 --- a/tests/Transports.Subscriptions.WebSockets.Tests/TestWebSocket.cs +++ b/tests/Transports.Subscriptions.WebSockets.Tests/TestWebSocket.cs @@ -1,178 +1,177 @@ using System.Net.WebSockets; -namespace GraphQL.Server.Transports.WebSockets.Tests +namespace GraphQL.Server.Transports.WebSockets.Tests; + +public class TestWebSocket : WebSocket { - public class TestWebSocket : WebSocket + public TestWebSocket() { - public TestWebSocket() - { - CurrentMessage = new ChunkedMemoryStream(); - } + CurrentMessage = new ChunkedMemoryStream(); + } - public override void Abort() - { - } + public override void Abort() + { + } - public override Task CloseAsync(WebSocketCloseStatus closeStatus, string statusDescription, CancellationToken cancellationToken) => Task.CompletedTask; + public override Task CloseAsync(WebSocketCloseStatus closeStatus, string statusDescription, CancellationToken cancellationToken) => Task.CompletedTask; - public override Task CloseOutputAsync(WebSocketCloseStatus closeStatus, string statusDescription, CancellationToken cancellationToken) => Task.CompletedTask; + public override Task CloseOutputAsync(WebSocketCloseStatus closeStatus, string statusDescription, CancellationToken cancellationToken) => Task.CompletedTask; - public override void Dispose() - { - } + public override void Dispose() + { + } - public override Task ReceiveAsync(ArraySegment buffer, CancellationToken cancellationToken) => throw new NotSupportedException(); + public override Task ReceiveAsync(ArraySegment buffer, CancellationToken cancellationToken) => throw new NotSupportedException(); - public override Task SendAsync(ArraySegment buffer, WebSocketMessageType messageType, bool endOfMessage, - CancellationToken cancellationToken) + public override Task SendAsync(ArraySegment buffer, WebSocketMessageType messageType, bool endOfMessage, + CancellationToken cancellationToken) + { + if (buffer.Array != null) { - if (buffer.Array != null) - { - CurrentMessage.Write(buffer.Array, buffer.Offset, buffer.Count); - } - - if (endOfMessage) - { - Messages.Add(CurrentMessage); - CurrentMessage = new ChunkedMemoryStream(); - } - - return Task.CompletedTask; + CurrentMessage.Write(buffer.Array, buffer.Offset, buffer.Count); } - internal List Messages { get; } = new List(); - private ChunkedMemoryStream CurrentMessage { get; set; } + if (endOfMessage) + { + Messages.Add(CurrentMessage); + CurrentMessage = new ChunkedMemoryStream(); + } - public override WebSocketCloseStatus? CloseStatus { get; } - public override string CloseStatusDescription { get; } = ""; - public override string SubProtocol { get; } = ""; - public override WebSocketState State { get; } = WebSocketState.Open; + return Task.CompletedTask; } - internal class ChunkedMemoryStream : Stream - { - private readonly List _chunks = new List(); - private int _positionChunk; - private int _positionOffset; - private long _position; + internal List Messages { get; } = new List(); + private ChunkedMemoryStream CurrentMessage { get; set; } - public override bool CanRead => true; + public override WebSocketCloseStatus? CloseStatus { get; } + public override string CloseStatusDescription { get; } = ""; + public override string SubProtocol { get; } = ""; + public override WebSocketState State { get; } = WebSocketState.Open; +} - public override bool CanSeek => true; +internal class ChunkedMemoryStream : Stream +{ + private readonly List _chunks = new List(); + private int _positionChunk; + private int _positionOffset; + private long _position; - public override bool CanWrite => true; + public override bool CanRead => true; - public override void Flush() - { - } + public override bool CanSeek => true; - public override long Length => _chunks.Sum(c => c.Length); + public override bool CanWrite => true; - public override long Position + public override void Flush() + { + } + + public override long Length => _chunks.Sum(c => c.Length); + + public override long Position + { + get => _position; + set { - get => _position; - set - { - _position = value; + _position = value; - _positionChunk = 0; + _positionChunk = 0; - while (_positionOffset != 0) - { - if (_positionChunk >= _chunks.Count) - throw new OverflowException(); + while (_positionOffset != 0) + { + if (_positionChunk >= _chunks.Count) + throw new OverflowException(); - if (_positionOffset < _chunks[_positionChunk].Length) - return; + if (_positionOffset < _chunks[_positionChunk].Length) + return; - _positionOffset -= _chunks[_positionChunk].Length; - _positionChunk++; - } + _positionOffset -= _chunks[_positionChunk].Length; + _positionChunk++; } } + } - public override int Read(byte[] buffer, int offset, int count) + public override int Read(byte[] buffer, int offset, int count) + { + int result = 0; + while (count != 0 && _positionChunk != _chunks.Count) { - int result = 0; - while (count != 0 && _positionChunk != _chunks.Count) + int fromChunk = Math.Min(count, _chunks[_positionChunk].Length - _positionOffset); + if (fromChunk != 0) { - int fromChunk = Math.Min(count, _chunks[_positionChunk].Length - _positionOffset); - if (fromChunk != 0) - { - Array.Copy(_chunks[_positionChunk], _positionOffset, buffer, offset, fromChunk); - offset += fromChunk; - count -= fromChunk; - result += fromChunk; - _position += fromChunk; - } - - _positionOffset = 0; - _positionChunk++; + Array.Copy(_chunks[_positionChunk], _positionOffset, buffer, offset, fromChunk); + offset += fromChunk; + count -= fromChunk; + result += fromChunk; + _position += fromChunk; } - return result; + _positionOffset = 0; + _positionChunk++; } - public override long Seek(long offset, SeekOrigin origin) - { - long newPos = 0; + return result; + } - switch (origin) - { - case SeekOrigin.Begin: - newPos = offset; - break; - case SeekOrigin.Current: - newPos = Position + offset; - break; - case SeekOrigin.End: - newPos = Length - offset; - break; - } + public override long Seek(long offset, SeekOrigin origin) + { + long newPos = 0; - Position = Math.Max(0, Math.Min(newPos, Length)); - return newPos; + switch (origin) + { + case SeekOrigin.Begin: + newPos = offset; + break; + case SeekOrigin.Current: + newPos = Position + offset; + break; + case SeekOrigin.End: + newPos = Length - offset; + break; } - public override void SetLength(long value) => throw new NotSupportedException(); + Position = Math.Max(0, Math.Min(newPos, Length)); + return newPos; + } - public override void Write(byte[] buffer, int offset, int count) + public override void SetLength(long value) => throw new NotSupportedException(); + + public override void Write(byte[] buffer, int offset, int count) + { + while (count != 0 && _positionChunk != _chunks.Count) { - while (count != 0 && _positionChunk != _chunks.Count) + int toChunk = Math.Min(count, _chunks[_positionChunk].Length - _positionOffset); + if (toChunk != 0) { - int toChunk = Math.Min(count, _chunks[_positionChunk].Length - _positionOffset); - if (toChunk != 0) - { - Array.Copy(buffer, offset, _chunks[_positionChunk], _positionOffset, toChunk); - offset += toChunk; - count -= toChunk; - _position += toChunk; - } - - _positionOffset = 0; - _positionChunk++; + Array.Copy(buffer, offset, _chunks[_positionChunk], _positionOffset, toChunk); + offset += toChunk; + count -= toChunk; + _position += toChunk; } - if (count != 0) - { - byte[] chunk = new byte[count]; - Array.Copy(buffer, offset, chunk, 0, count); - _chunks.Add(chunk); - _positionChunk = _chunks.Count; - _position += count; - } + _positionOffset = 0; + _positionChunk++; } - public byte[] ToArray() + if (count != 0) + { + byte[] chunk = new byte[count]; + Array.Copy(buffer, offset, chunk, 0, count); + _chunks.Add(chunk); + _positionChunk = _chunks.Count; + _position += count; + } + } + + public byte[] ToArray() + { + using (var ms = new MemoryStream()) { - using (var ms = new MemoryStream()) + foreach (byte[] bytes in _chunks) { - foreach (byte[] bytes in _chunks) - { - ms.Write(bytes, 0, bytes.Length); - } - return ms.ToArray(); + ms.Write(bytes, 0, bytes.Length); } + return ms.ToArray(); } } } diff --git a/tests/Transports.Subscriptions.WebSockets.Tests/WebSocketWriterPipelineTests.cs b/tests/Transports.Subscriptions.WebSockets.Tests/WebSocketWriterPipelineTests.cs index bb4bdba8..d6e59adb 100644 --- a/tests/Transports.Subscriptions.WebSockets.Tests/WebSocketWriterPipelineTests.cs +++ b/tests/Transports.Subscriptions.WebSockets.Tests/WebSocketWriterPipelineTests.cs @@ -6,234 +6,233 @@ using Newtonsoft.Json.Serialization; using JsonSerializerSettings = GraphQL.NewtonsoftJson.JsonSerializerSettings; -namespace GraphQL.Server.Transports.WebSockets.Tests +namespace GraphQL.Server.Transports.WebSockets.Tests; + +public class WebSocketWriterPipelineFacts { - public class WebSocketWriterPipelineFacts + private readonly TestWebSocket _testWebSocket; + + public WebSocketWriterPipelineFacts() { - private readonly TestWebSocket _testWebSocket; + _testWebSocket = new TestWebSocket(); + } - public WebSocketWriterPipelineFacts() + public static IEnumerable TestData => + new List { - _testWebSocket = new TestWebSocket(); - } - - public static IEnumerable TestData => - new List + new object[] { - new object[] + new OperationMessage { - new OperationMessage + Payload = new ExecutionResult { - Payload = new ExecutionResult + Executed = true, + Data = new TestMessage { - Executed = true, - Data = new TestMessage - { - Content = "Hello world", - SentAt = new DateTimeOffset(2018, 12, 12, 10, 0, 0, TimeSpan.Zero) - } + Content = "Hello world", + SentAt = new DateTimeOffset(2018, 12, 12, 10, 0, 0, TimeSpan.Zero) } - }, - 83 + } }, - new object[] + 83 + }, + new object[] + { + new OperationMessage { - new OperationMessage + Payload = new ExecutionResult { - Payload = new ExecutionResult + Executed = true, + Data = Enumerable.Repeat(new TestMessage { - Executed = true, - Data = Enumerable.Repeat(new TestMessage - { - Content = "Hello world", - SentAt = new DateTimeOffset(2018, 12, 12, 10, 0, 0, TimeSpan.Zero) - }, 10) - } - }, - 652 + Content = "Hello world", + SentAt = new DateTimeOffset(2018, 12, 12, 10, 0, 0, TimeSpan.Zero) + }, 10) + } }, - new object[] + 652 + }, + new object[] + { + new OperationMessage { - new OperationMessage + Payload = new ExecutionResult { - Payload = new ExecutionResult + Executed = true, + Data = Enumerable.Repeat(new TestMessage { - Executed = true, - Data = Enumerable.Repeat(new TestMessage - { - Content = "Hello world", - SentAt = new DateTimeOffset(2018, 12, 12, 10, 0, 0, TimeSpan.Zero) - }, 16_000) - } - }, - // About 1 megabyte - 1_008_022 + Content = "Hello world", + SentAt = new DateTimeOffset(2018, 12, 12, 10, 0, 0, TimeSpan.Zero) + }, 16_000) + } }, - new object[] + // About 1 megabyte + 1_008_022 + }, + new object[] + { + new OperationMessage { - new OperationMessage + Payload = new ExecutionResult { - Payload = new ExecutionResult + Executed = true, + Data = Enumerable.Repeat(new TestMessage { - Executed = true, - Data = Enumerable.Repeat(new TestMessage - { - Content = "Hello world", - SentAt = new DateTimeOffset(2018, 12, 12, 10, 0, 0, TimeSpan.Zero) - }, 160_000) - } - }, - // About 10 megabytes - 10_080_022 + Content = "Hello world", + SentAt = new DateTimeOffset(2018, 12, 12, 10, 0, 0, TimeSpan.Zero) + }, 160_000) + } }, - new object[] + // About 10 megabytes + 10_080_022 + }, + new object[] + { + new OperationMessage { - new OperationMessage + Payload = new ExecutionResult { - Payload = new ExecutionResult + Executed = true, + Data = Enumerable.Repeat(new TestMessage { - Executed = true, - Data = Enumerable.Repeat(new TestMessage - { - Content = "Hello world", - SentAt = new DateTimeOffset(2018, 12, 12, 10, 0, 0, TimeSpan.Zero) - }, 1_600_000) - } - }, - // About 100 megabytes - 100_800_022 + Content = "Hello world", + SentAt = new DateTimeOffset(2018, 12, 12, 10, 0, 0, TimeSpan.Zero) + }, 1_600_000) + } }, - }; + // About 100 megabytes + 100_800_022 + }, + }; - [Fact] - public async Task should_post_single_message() + [Fact] + public async Task should_post_single_message() + { + var webSocketWriterPipeline = CreateWebSocketWriterPipeline(new CamelCaseNamingStrategy()); + var message = new OperationMessage { - var webSocketWriterPipeline = CreateWebSocketWriterPipeline(new CamelCaseNamingStrategy()); - var message = new OperationMessage + Payload = new ExecutionResult { - Payload = new ExecutionResult + Executed = true, + Data = new TestMessage { - Executed = true, - Data = new TestMessage - { - Content = "Hello world", - SentAt = new DateTimeOffset(2018, 12, 12, 10, 0, 0, TimeSpan.Zero) - } + Content = "Hello world", + SentAt = new DateTimeOffset(2018, 12, 12, 10, 0, 0, TimeSpan.Zero) } - }; - Assert.True(webSocketWriterPipeline.Post(message)); - await webSocketWriterPipeline.Complete(); - await webSocketWriterPipeline.Completion; - Assert.Single(_testWebSocket.Messages); + } + }; + Assert.True(webSocketWriterPipeline.Post(message)); + await webSocketWriterPipeline.Complete(); + await webSocketWriterPipeline.Completion; + Assert.Single(_testWebSocket.Messages); - string resultingJson = Encoding.UTF8.GetString(_testWebSocket.Messages.First().ToArray()); - Assert.Equal( - "{\"payload\":{\"data\":{\"content\":\"Hello world\",\"sentAt\":\"2018-12-12T10:00:00+00:00\"}}}", - resultingJson); - } + string resultingJson = Encoding.UTF8.GetString(_testWebSocket.Messages.First().ToArray()); + Assert.Equal( + "{\"payload\":{\"data\":{\"content\":\"Hello world\",\"sentAt\":\"2018-12-12T10:00:00+00:00\"}}}", + resultingJson); + } - [Fact] - public async Task should_post_array_of_10_messages() + [Fact] + public async Task should_post_array_of_10_messages() + { + var webSocketWriterPipeline = CreateWebSocketWriterPipeline(new CamelCaseNamingStrategy()); + var message = new OperationMessage { - var webSocketWriterPipeline = CreateWebSocketWriterPipeline(new CamelCaseNamingStrategy()); - var message = new OperationMessage + Payload = new ExecutionResult { - Payload = new ExecutionResult + Executed = true, + Data = Enumerable.Repeat(new TestMessage { - Executed = true, - Data = Enumerable.Repeat(new TestMessage - { - Content = "Hello world", - SentAt = new DateTimeOffset(2018, 12, 12, 10, 0, 0, TimeSpan.Zero) - }, 10) - } - }; - Assert.True(webSocketWriterPipeline.Post(message)); - await webSocketWriterPipeline.Complete(); - await webSocketWriterPipeline.Completion; - Assert.Single(_testWebSocket.Messages); + Content = "Hello world", + SentAt = new DateTimeOffset(2018, 12, 12, 10, 0, 0, TimeSpan.Zero) + }, 10) + } + }; + Assert.True(webSocketWriterPipeline.Post(message)); + await webSocketWriterPipeline.Complete(); + await webSocketWriterPipeline.Completion; + Assert.Single(_testWebSocket.Messages); - string resultingJson = Encoding.UTF8.GetString(_testWebSocket.Messages.First().ToArray()); - Assert.Equal( - "{\"payload\":" + - "{\"data\":[" + - "{\"content\":\"Hello world\",\"sentAt\":\"2018-12-12T10:00:00+00:00\"}," + - "{\"content\":\"Hello world\",\"sentAt\":\"2018-12-12T10:00:00+00:00\"}," + - "{\"content\":\"Hello world\",\"sentAt\":\"2018-12-12T10:00:00+00:00\"}," + - "{\"content\":\"Hello world\",\"sentAt\":\"2018-12-12T10:00:00+00:00\"}," + - "{\"content\":\"Hello world\",\"sentAt\":\"2018-12-12T10:00:00+00:00\"}," + - "{\"content\":\"Hello world\",\"sentAt\":\"2018-12-12T10:00:00+00:00\"}," + - "{\"content\":\"Hello world\",\"sentAt\":\"2018-12-12T10:00:00+00:00\"}," + - "{\"content\":\"Hello world\",\"sentAt\":\"2018-12-12T10:00:00+00:00\"}," + - "{\"content\":\"Hello world\",\"sentAt\":\"2018-12-12T10:00:00+00:00\"}," + - "{\"content\":\"Hello world\",\"sentAt\":\"2018-12-12T10:00:00+00:00\"}" + - "]}" + - "}", - resultingJson); - } + string resultingJson = Encoding.UTF8.GetString(_testWebSocket.Messages.First().ToArray()); + Assert.Equal( + "{\"payload\":" + + "{\"data\":[" + + "{\"content\":\"Hello world\",\"sentAt\":\"2018-12-12T10:00:00+00:00\"}," + + "{\"content\":\"Hello world\",\"sentAt\":\"2018-12-12T10:00:00+00:00\"}," + + "{\"content\":\"Hello world\",\"sentAt\":\"2018-12-12T10:00:00+00:00\"}," + + "{\"content\":\"Hello world\",\"sentAt\":\"2018-12-12T10:00:00+00:00\"}," + + "{\"content\":\"Hello world\",\"sentAt\":\"2018-12-12T10:00:00+00:00\"}," + + "{\"content\":\"Hello world\",\"sentAt\":\"2018-12-12T10:00:00+00:00\"}," + + "{\"content\":\"Hello world\",\"sentAt\":\"2018-12-12T10:00:00+00:00\"}," + + "{\"content\":\"Hello world\",\"sentAt\":\"2018-12-12T10:00:00+00:00\"}," + + "{\"content\":\"Hello world\",\"sentAt\":\"2018-12-12T10:00:00+00:00\"}," + + "{\"content\":\"Hello world\",\"sentAt\":\"2018-12-12T10:00:00+00:00\"}" + + "]}" + + "}", + resultingJson); + } - [Theory] - [MemberData(nameof(TestData))] - public async Task should_post_for_any_message_length(OperationMessage message, long expectedLength) - { - var webSocketWriterPipeline = CreateWebSocketWriterPipeline(new CamelCaseNamingStrategy()); - Assert.True(webSocketWriterPipeline.Post(message)); - await webSocketWriterPipeline.Complete(); - await webSocketWriterPipeline.Completion; - Assert.Single(_testWebSocket.Messages); - _testWebSocket.Messages.First().Length.ShouldBe(expectedLength); - } + [Theory] + [MemberData(nameof(TestData))] + public async Task should_post_for_any_message_length(OperationMessage message, long expectedLength) + { + var webSocketWriterPipeline = CreateWebSocketWriterPipeline(new CamelCaseNamingStrategy()); + Assert.True(webSocketWriterPipeline.Post(message)); + await webSocketWriterPipeline.Complete(); + await webSocketWriterPipeline.Completion; + Assert.Single(_testWebSocket.Messages); + _testWebSocket.Messages.First().Length.ShouldBe(expectedLength); + } - [Theory] - [MemberData(nameof(TestData))] - public async Task should_send_for_any_message_length(OperationMessage message, long expectedLength) - { - var webSocketWriterPipeline = CreateWebSocketWriterPipeline(new CamelCaseNamingStrategy()); - await webSocketWriterPipeline.SendAsync(message); - await webSocketWriterPipeline.Complete(); - await webSocketWriterPipeline.Completion; - Assert.Single(_testWebSocket.Messages); - Assert.Equal(expectedLength, _testWebSocket.Messages.First().Length); - } + [Theory] + [MemberData(nameof(TestData))] + public async Task should_send_for_any_message_length(OperationMessage message, long expectedLength) + { + var webSocketWriterPipeline = CreateWebSocketWriterPipeline(new CamelCaseNamingStrategy()); + await webSocketWriterPipeline.SendAsync(message); + await webSocketWriterPipeline.Complete(); + await webSocketWriterPipeline.Completion; + Assert.Single(_testWebSocket.Messages); + Assert.Equal(expectedLength, _testWebSocket.Messages.First().Length); + } - [Fact] - public async Task should_support_correct_case() + [Fact] + public async Task should_support_correct_case() + { + var webSocketWriterPipeline = CreateWebSocketWriterPipeline(new DefaultNamingStrategy()); + var operationMessage = new OperationMessage { - var webSocketWriterPipeline = CreateWebSocketWriterPipeline(new DefaultNamingStrategy()); - var operationMessage = new OperationMessage + Id = "78F15F13-CA90-4BA6-AFF5-990C23FA882A", + Type = "Type", + Payload = new ExecutionResult { - Id = "78F15F13-CA90-4BA6-AFF5-990C23FA882A", - Type = "Type", - Payload = new ExecutionResult + Executed = true, + Data = new TestMessage { - Executed = true, - Data = new TestMessage - { - Content = "Hello world", - SentAt = new DateTimeOffset(2018, 12, 12, 10, 0, 0, TimeSpan.Zero) - } + Content = "Hello world", + SentAt = new DateTimeOffset(2018, 12, 12, 10, 0, 0, TimeSpan.Zero) } - }; + } + }; - await webSocketWriterPipeline.SendAsync(operationMessage); - await webSocketWriterPipeline.Complete(); - await webSocketWriterPipeline.Completion; - Assert.Single(_testWebSocket.Messages); - string resultingJson = Encoding.UTF8.GetString(_testWebSocket.Messages.First().ToArray()); - Assert.Equal( - "{\"type\":\"Type\",\"id\":\"78F15F13-CA90-4BA6-AFF5-990C23FA882A\",\"payload\":{\"data\":{\"Content\":\"Hello world\",\"SentAt\":\"2018-12-12T10:00:00+00:00\"}}}", - resultingJson); - } + await webSocketWriterPipeline.SendAsync(operationMessage); + await webSocketWriterPipeline.Complete(); + await webSocketWriterPipeline.Completion; + Assert.Single(_testWebSocket.Messages); + string resultingJson = Encoding.UTF8.GetString(_testWebSocket.Messages.First().ToArray()); + Assert.Equal( + "{\"type\":\"Type\",\"id\":\"78F15F13-CA90-4BA6-AFF5-990C23FA882A\",\"payload\":{\"data\":{\"Content\":\"Hello world\",\"SentAt\":\"2018-12-12T10:00:00+00:00\"}}}", + resultingJson); + } - private WebSocketWriterPipeline CreateWebSocketWriterPipeline(NamingStrategy namingStrategy) - { - return new WebSocketWriterPipeline(_testWebSocket, new GraphQLSerializer( - new JsonSerializerSettings - { - ContractResolver = new GraphQLContractResolver(new ErrorInfoProvider()) { NamingStrategy = namingStrategy }, - NullValueHandling = NullValueHandling.Ignore, - Formatting = Formatting.None - })); - } + private WebSocketWriterPipeline CreateWebSocketWriterPipeline(NamingStrategy namingStrategy) + { + return new WebSocketWriterPipeline(_testWebSocket, new GraphQLSerializer( + new JsonSerializerSettings + { + ContractResolver = new GraphQLContractResolver(new ErrorInfoProvider()) { NamingStrategy = namingStrategy }, + NullValueHandling = NullValueHandling.Ignore, + Formatting = Formatting.None + })); } } diff --git a/tests/Transports.Subscriptions.WebSockets.Tests/WebSocketsConnectionFacts.cs b/tests/Transports.Subscriptions.WebSockets.Tests/WebSocketsConnectionFacts.cs index 80e83c63..5224bec8 100644 --- a/tests/Transports.Subscriptions.WebSockets.Tests/WebSocketsConnectionFacts.cs +++ b/tests/Transports.Subscriptions.WebSockets.Tests/WebSocketsConnectionFacts.cs @@ -3,63 +3,62 @@ using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.Hosting; -namespace GraphQL.Server.Transports.WebSockets.Tests +namespace GraphQL.Server.Transports.WebSockets.Tests; + +public class WebSocketsConnectionFacts : IDisposable { - public class WebSocketsConnectionFacts : IDisposable + public WebSocketsConnectionFacts() { - public WebSocketsConnectionFacts() - { - _host = Host - .CreateDefaultBuilder() - .ConfigureWebHost(webHostBuilder => - { - webHostBuilder - .UseTestServer() - .UseStartup(); - }) - .Start(); + _host = Host + .CreateDefaultBuilder() + .ConfigureWebHost(webHostBuilder => + { + webHostBuilder + .UseTestServer() + .UseStartup(); + }) + .Start(); - _server = _host.GetTestServer(); - } + _server = _host.GetTestServer(); + } - private readonly IHost _host; - private readonly TestServer _server; + private readonly IHost _host; + private readonly TestServer _server; - private Task ConnectAsync(string protocol) - { - var client = _server.CreateWebSocketClient(); - client.ConfigureRequest = request => request.Headers.Add("Sec-WebSocket-Protocol", protocol); - return client.ConnectAsync(new Uri("http://localhost/graphql"), CancellationToken.None); - } + private Task ConnectAsync(string protocol) + { + var client = _server.CreateWebSocketClient(); + client.ConfigureRequest = request => request.Headers.Add("Sec-WebSocket-Protocol", protocol); + return client.ConnectAsync(new Uri("http://localhost/graphql"), CancellationToken.None); + } - [Fact] - public async Task Should_accept_websocket_connection() - { - /* Given */ - /* When */ - var socket = await ConnectAsync("graphql-ws"); + [Fact] + public async Task Should_accept_websocket_connection() + { + /* Given */ + /* When */ + var socket = await ConnectAsync("graphql-ws"); - /* Then */ - Assert.Equal(WebSocketState.Open, socket.State); - } + /* Then */ + Assert.Equal(WebSocketState.Open, socket.State); + } - [Fact] - public async Task Should_not_accept_websocket_with_wrong_protocol() - { - /* Given */ - /* When */ - var socket = await ConnectAsync("do-not-accept"); - var segment = new ArraySegment(new byte[1024]); - var received = await socket.ReceiveAsync(segment, CancellationToken.None); + [Fact] + public async Task Should_not_accept_websocket_with_wrong_protocol() + { + /* Given */ + /* When */ + var socket = await ConnectAsync("do-not-accept"); + var segment = new ArraySegment(new byte[1024]); + var received = await socket.ReceiveAsync(segment, CancellationToken.None); - /* Then */ - received.CloseStatus.ShouldBe(WebSocketCloseStatus.ProtocolError); - } + /* Then */ + received.CloseStatus.ShouldBe(WebSocketCloseStatus.ProtocolError); + } - public void Dispose() - { - _server.Dispose(); - _host.Dispose(); - } + public void Dispose() + { + _server.Dispose(); + _host.Dispose(); } }